1

Does the absolute value protect the following code from the Environment.TickCount wrap?

If Math.Abs((Environment.TickCount And Int32.MaxValue) - StartTime) >= Interval Then
    StartTime = Environment.TickCount And Int32.MaxValue ' set up next interval
    ...
    ...
    ...
End If

Is there a better method of guarding against the Environment.TickCount wrap-around?

(This is .NET 1.1.)

Edit - Modified code according to Microsoft Environment.TickCount help.

A876
  • 471
  • 5
  • 8
user79755
  • 2,623
  • 5
  • 30
  • 36

8 Answers8

8

Avoiding the wrapping problem is easy, provided that the time span you want to measure is no more than 24.8 days (anything longer can't be represented in a signed integer). In C#:

int start = Environment.TickCount;
DoLongRunningOperation();
int elapsedTime = Environment.TickCount - start;

I am not very familiar with VB.NET, but as long as you use unchecked math, this will work and you don't have to worry about the wrap.

For example, if Environment.TickCount wraps from 2147483600 to -2147483596, it doesn't matter. You can still compute the difference between them, which is 100 milliseconds.

IamIC
  • 17,747
  • 20
  • 91
  • 154
Qwertie
  • 16,354
  • 20
  • 105
  • 148
  • Still wrapping after 50 days. – usr May 09 '12 at 20:14
  • Using this method you can track 50 days at most. – usr Jun 06 '12 at 19:16
  • 1
    Yes I know this was asked years ago, but, if you need to track more than 50 days using this method, why not keep tabs on how many times the counter has wrapped. Then you can run it for 50 * Int32.MaxValue days. Poll to see if it has wrapped every so often and keep track. – Patrick Allwood Jan 06 '14 at 13:29
4

Since I was looking into this and stumbled onto this page I'll share a small benchmark I made.

When running the code ignore the first 1000-result from each test (used for warmup). Also ignore the number printed after "ignore:". Its just there just to ensure no compiler optimization is done that would affect the result.

Results on my computer:

Test1 1000000: 7ms    - Environment.TickCount
Test2 1000000: 1392ms - DateTime.Now.Ticks
Test3 1000000: 933ms  - Stopwatch.ElapsedMilliseconds

Both DateTime.Now.Ticks (test 2) and Stopwatch.ElapsedMilliseconds (test 3) are considerably slower than Environment.TickCount (test 1), but not enough to be noticeable unless you are performing a lot of calculations. For example I was investigating this because I need a cheap way of getting time in tight game loops.

I think my solution will have to be something like (untested):

var elapsed = Environment.TickCount - start;
if (elapsed < 0)
   elapsed = Int32.MaxValue - start + Environment.TickCount;

Benchmark code:

void Main()
{
    Test1(1000);
    Test1(1000000);
    Test2(1000);
    Test2(1000000);
    Test3(1000);
    Test3(1000000);
}

void Test1(int c) {
    var sw = new Stopwatch();
    sw.Start();
    long sum = 0;
    for (var i = 0; i < c; i++)
    {
        sum += Environment.TickCount;
    }
    sw.Stop();
    Console.WriteLine("Test1 " + c + ": " + sw.ElapsedMilliseconds + "ms   (ignore: " + sum + ")");}

void Test2(int c) {
    var sw = new Stopwatch();
    sw.Start();
    long sum = 0;
    for (var i = 0; i < c; i++)
    {
        sum += DateTime.Now.Ticks;
    }
    sw.Stop();
    Console.WriteLine("Test2 " + c + ": " + sw.ElapsedMilliseconds + "ms   (ignore: " + sum + ")");
}
void Test3(int c) {
    var sw = new Stopwatch();
    sw.Start();
    long sum = 0;
    var stopwatch = new Stopwatch();
    stopwatch.Start();
    for (var i = 0; i < c; i++)
    {
        sum += stopwatch.ElapsedMilliseconds;
    }
    sw.Stop();
    Console.WriteLine("Test3 " + c + ": " + sw.ElapsedMilliseconds + "ms   (ignore: " + sum + ")");
}
Tedd Hansen
  • 12,074
  • 14
  • 61
  • 97
2

Just use the stopwatch:

Stopwatch stopWatch = new Stopwatch();
stopWatch.Start();
Thread.Sleep(10000);
stopWatch.Stop();
// Get the elapsed time as a TimeSpan value.
TimeSpan ts = stopWatch.Elapsed;

It will return an int64 and not an int32. Plus it's easier to understand.

Carra
  • 17,808
  • 7
  • 62
  • 75
  • I know this is 3 years old, but Stopwatch/QueryPerformanceCounter is dead IMO because almost all PCs have multiple processors now, and it isn't reliable in that case unless you force your thread to run on one process.. but then that probably invalidates what you wanted to test for anyway since your release version will probably not have that restriction. – eselk Jun 19 '12 at 19:27
  • 1
    @eselk this is not true for all versions of windows above Windows XP. See [here](https://msdn.microsoft.com/en-us/library/windows/desktop/dn553408(v=vs.85).aspx#Does_QPC_reliably_work_on_multi-processor_systems__multi-core_system__and_________systems_with_hyper-threading_). – Karsten May 02 '17 at 16:02
  • I misunderstood the comment from Karsten: More clear: you can safely use and it is recommended to use Stopwatch() for **Windows XP AND above** !! According to that linked MSDN article by Karsten there are only few processors where Performancecounters (and Stopwatch) takes a long time: "On a relatively small number of platforms that can't use the TSC register as the QPC basis, [..]., acquiring high resolution time stamps can be significantly more expensive than acquiring time stamps with lower resolution. If resolution of 10 to 16 milliseconds is sufficient, you can use GetTickCount64...." – Philm May 03 '17 at 17:26
1

I'm not sure what you're trying to accomplish. It looks like you are trying to detect if a particular interval has occured and if so execute some specific logic.

This is likely to cause you no end of pain if you use Environment.TickCount for this purpose. As you pointed out this value can and will wrap roughly every 25 days. There is no way to prevent this from happening and if you try to you'll end up with bugs in your code.

Instead why not do the following:

  • Use an actual Timer for the internal and execution of the event
  • Store the StartTime as a DateTime (or just Date in VB) value. This value has a much longer range. It's highly unlikely that your app will run long enough for this value to wrap around(OS will need a reboot long before then :) ).
JaredPar
  • 733,204
  • 149
  • 1,241
  • 1,454
0

I assume StartTime is a positive signed integer; I assume you earlier did StartTime = (Environment.TickCount And Int32.MaxValue).

Abs() does not fix the problem. If Interval < 12.43 days, the code will time correctly while StartTime is small. But some of the time, whenever StartTime > (24.86 days - Interval), the test will pass earlier than it should, the moment that (TickCount And Int32.MaxValue) rolls over to 0. Additionally, if Interval > 12.43 days AND (TickCount And Int32.MaxValue) is close to 12.43 days, the test will never pass.

Doing Environment.TickCount And Int32.MaxValue is not necessary and not helpful. It makes a positive signed integer32 representing time in ms that wraps around to 0 every 24.86 days. The alternatives are harder to understand, but they let you time intervals up to 49.71 days with no difficulty. The raw Environment.TickCount wraps around to Int32.MinValue after 24.86 days and every 49.71 days after that. Casting unchecked((Uint32)Environment.TickCount) makes a [positive] unsigned integer32 representing time in ms that wraps around to 0 every 49.71 days. (Because Environment.TickCount calls the Windows function GetTickCount(), which returns a DWORD (UInt32), and unchecked-casts it to Int32.)

The wraparound of Environment.TickCount does not impair measurement of elapsed time. Unchecked subtraction always gives the correct result.

You can compute elapsed time as signed int32 (-24.86 to +24.86 days), more useful if you are comparing independent times or potentially mixing future and past; or unsigned int32 (0 to +49.71 days). You can capture T0 as signed int32 or unsigned int32; it makes no difference to the results. You only need [unchecked] casting if T0 or elapsed time are unsigned; with signed T0 and signed elapsed time, you need no casts.

Capturing T0 signed: (C#)

...
int iT0 = Environment.TickCount;  // T0 is NOW.
...
iElapsed = unchecked(Environment.TickCount - iT0)  // -24.81 to +24.81 days
uiElapsed = unchecked((uint)Environment.TickCount - (uint)iT0)  // 0 to +49.71 days
if (uiElapsed >= uiTimeLimit)  // IF time expired,
{
    iT0 = Environment.TickCount;  // For the next interval, new T0 is NOW.
    ...
}

Capturing T0 unsigned (my preference; Environment.TickCount should never have been signed):

...
uint uiT0 = unchecked((uint)Environment.TickCount);  // T0 is NOW.
...
iElapsed = unchecked(Environment.TickCount - (int)uiT0)  // -24.81 to +24.81 days
uiElapsed = unchecked((uint)Environment.TickCount - uiT0)  // 0 to +49.71 days
if (uiElapsed >= uiTimeLimit)  // IF time expired,
{
    uiT0 = unchecked((uint)Environment.TickCount);  // For the next interval, new T0 is NOW.
    ...
}

If your TimeLimit is close to the wraparound limit (24.81 or 49.71 days), it is possible for time to expire without the test ever passing. You have to test at least once while Elapsed >= TimeLimit. (If you are not sure of testing often enough, you can add a backup test on Elapsed. If Elapsed ever decreases, it has wrapped, so the time must be up.)

=====

To time intervals longer than 49.71 days, you can count how many times uiElapsed wraps around, or you can count how many times Environment.TickCount wraps around. You can compose the counters into a 64-bit value, emulating GetTickCount64() (only available in on Windows Vista and newer). A 64-bit value has full range (292 million years) and full resolution (1ms). Or you can make a 32-bit value that has reduced range and/or resolution. The code that checks for wrapping must execute at least once every 49.71 days, to ensure that no wrap goes detected.

uint uiTickCountPrev = 0;
uint uiTickWrap = 0;
Int64 TickCount64;
...
uiTickCount = unchecked((uint)Environment.TickCount)  // 0 to +49.71 days
if (uiTickCount < uiTickCountPrev)  // IF uiElapsed decreased,
    uiWrapcount++;  count that uiElapsed wrapped.
uiElapsedPrev = uiElapsed;  // Save the previous value.
TickCount64 = (Int64)uiTickWrap << 32 + (Int64)uiTickCount;

Notes:

Environment.TickCount gives time since boot, in ms, with 10-16ms resolution.

The unchecked Int32 difference gives the time difference, -24.81 to +24.81 days, with wrap-around. Unchecked subtraction of 32-bit integers overflows and wraps around to the correct value. The sign of Environment.TickCount never matters. Example, wraparound by one: iBase = Environment.TickCount gets Int32.MaxValue; one tick later, Environment.TickCount wraps to Int32.MinValue; and unchecked(Environment.TickCount - iBase) yields 1.

The unchecked UInt32 difference gives the time difference, 0 to +49.71 days, with wrap-around. (Elapsed time can never be negative, so this is the better choice.) Unchecked subtraction of unsigned 32-bit integers overflows and wraps around to the correct value. Example, wraparound by one: iBase = Environment.TickCount gets UInt32.MaxValue; one tick later, Environment.TickCount wraps to UInt32.MinValue; and unchecked(Environment.TickCount - iBase) yields 1.

A876
  • 471
  • 5
  • 8
  • `unchecked` doesn't seem to be necessary for the cast to work as expected. It wraps automatically. – ygoe Mar 20 '19 at 17:09
0

Stopwatch is the most appropriate choice for most purposes. Environment.TickCount makes only sense for rare purposes where:

  1. The needed time for time measurement itself is time critical and you need best performance because you have to measure multiple million or billion times. Here Tickcount has advantages, because it can be approx. 20-25 times quicker than Stopwatch() (measured on my i7 machine).

  2. But you don't need to measure more accurate than 1 milliscecond

  3. You are sure that you have not to count more than 24 days, because this is the max. (rollover) time of 32-bit int for TickCount.

Normally this would be not the case, and at least point 1. doesn't matter. Stopwatch (which measures normally at least about 1000 times more precisely (microsecond range and better)) is generally the most appropriate choice.

Besides .ElapsedMilliseconds you can also use the static method
Stopwatch.GetTimestamp();

Here is why: For calling Stopwatch 10 million times in a loop on my older i7 1.3 seconds are needed. GetTickCount64 is slightly quicker with little more than 1 second for the same. So choose one and don't have to struggle with the 32-bit stuff.

Of course the given performances are just approximations on one (my) hardware and can differ.

But Microsoft states to do all to make Stopwatch (performance counters) best on all systems. It seems unlikely that someone easily beats that:

https://msdn.microsoft.com/en-us/library/windows/desktop/dn553408%28v=vs.85%29.aspx?f=255&MSPPError=-2147217396#Does_QPC_reliably_work_on_multi-processor_systems__multi-core_system__and_________systems_with_hyper-threading_

Quotations:

"Is QPC accuracy affected by processor frequency changes caused by power management or Turbo Boost technology? No. If the processor has an invariant TSC, the QPC is not affected by these sort of changes. [...]"

"Does QPC reliably work on multi-processor systems, multi-core system, and systems with hyper-threading? Yes

How do I determine and validate that QPC works on my machine? You don't need to perform such checks. [...] "

Nevertheless reading that gives a lot of more detailed information, especially to performance differences in hardware and Windows versions. But this may be primarily important, if you need real-time performance where Windows is not made-for and surely not best suitable.

Philm
  • 3,448
  • 1
  • 29
  • 28
0

Depending on your accuracy, why don't you just use:

DateTime.Now.Ticks

There's no wrapping for this Ticks.

Keltex
  • 26,220
  • 11
  • 79
  • 111
0

Your code will most likely not work. If the app was to run on Vista or a newer Windows version, one could use GetTickCount64, but I assume that this is not the case, given that you use .NET 1.1.

Your code should not check for equality with the interval, as this will likely go wrong. Use a Int64 to store the ticks, store the last processed tick in a variable, and if this variable is greater than the new tick value you've got a wrap and need to increment your total tick base (the Int64) - assuming that you check the tick more often than 1 every 12 days... ;)

Lucero
  • 59,176
  • 9
  • 122
  • 152