Click here to Skip to main content
12,067,928 members (27,108 online)
Click here to Skip to main content
Add your own
alternative version

Tagged as

Stats

38.3K views
35 bookmarked
Posted

Obtaining Microsecond Precision in .NET

, 11 Apr 2013 CPOL
Rate this:
Please Sign up or sign in to vote.
Obtaining microsecond precision using .NET without Platform Invoke.

Introduction

It is has been widely toted that Microsecond Precision (μs) scale time is not possible on .NET or Mono due to many issues which I will not endeavor into explaining.

Based on some of this I originally had setup a task for myself to write a good portable μs scale timer which performed the necessary platform invocation.

After I was done I realized that there is nothing "scientifically" stopping .NET from having this precision based on the fact that the caller executes the invocation and obtains the result and the GC cannot interrupt platform invocation calls so long as you do not pass a managed type.

E.g., if I pass a plain old pointer to a un-managed function there is nothing for the GC to interrupt or stop unless the Kernel itself interrupts the call for something.

I originally though about using unsafe code but then I realized that I was just going closer to using platform invocation.

I thought about trying to obtain precise clock cycles using a static constructor which forced a GC and then ran to determine things like call overhead and whatnot but I felt that there was more time being spent on trying to obtain information then actually sleeping for the user which was the goal.

I then realized something even more bold and interesting... Sockets have a microsecond precision due to signaling and they are usable from the .NET Framework and there is a Poll method which actually accepts the amount of time in Microseconds (μs).

After some quick tests I realized I had something which was a lightweight sealed class with all static members with no more resources than a single socket.

I tricked the socket into always being busy and then I used the Poll method to obtain the desired sleep time in Microsecond Precision (μs).

I want to know what everyone thinks about this and if anyone sees anything glaring out at me which I did not also take into account.

Here is the class code and testing code complete with platform invocation methods (found here on Stack Overflow @ usleep is obsolte...) for comparison and testing.

#region Cross Platform μTimer

    /// <summary>
    /// A Cross platform implementation which can delay time on the microsecond(μs) scale.
    /// It operates at a frequencies which are faster then most Platform Invoke results can provide due to the use of Kernel Calls under the hood.
    /// Requires Libc.so@usleep on Mono and QueryPerformanceCounter on Windows for uSleep static
    /// </summary>
    /// <notes>A Tcp Socket will be created on port 7777 by default to help keep track of time. No connections will be recieved from this socket.</notes>
    public sealed class μTimer : IDisposable
    {
        #region Not Applicable for the MicroFramework
#if(!MF)

        #region Uncesessary Interop (Left for Comparison)
#if MONO
        using System.Runtime.InteropServices;
        [System.Runtime.InteropServices.DllImport("libc.so")] //.a , Not Portable
        static extern int usleep (uint amount);

        ///<notes>The type useconds_t is an unsigned integer type capable of holding integers in the range [0,1000000]. Programs will be more portable if they never mention this type explicitly. </notes>
        void uSleep(int waitTime) { usleep(waitTime); }
#else
        [System.Runtime.InteropServices.DllImport("Kernel32.dll")]
        static extern bool QueryPerformanceCounter(out long lpPerformanceCount);

        [System.Runtime.InteropServices.DllImport("Kernel32.dll")]
        static extern bool QueryPerformanceFrequency(out long lpFrequency);

        /// <summary>
        /// Performs a sleep using a plaform dependent but proven method
        /// </summary>
        /// <param name="amount">The amount of time to sleep in microseconds(μs)</param>
        public static void uSleep(TimeSpan amount) { μTimer.uSleep(((int)(amount.TotalMilliseconds * 1000))); }

        /// <summary>
        /// Performs uSleep by convention of waiting on performance couters
        /// </summary>
        /// <param name="waitTime">The amount of time to wait</param>
        public static void uSleep(int waitTime)
        {
            long time1 = 0, time2 = 0, freq = 0;

            QueryPerformanceCounter(out time1);
            QueryPerformanceFrequency(out freq);

            do
            {
                QueryPerformanceCounter(out time2);
            } while ((time2 - time1) < waitTime);
        }
#endif
        #endregion
#endif
        #endregion

        #region Statics

        //Who but me
        const ushort Port = 7777;

        //Since System.Timespan.TickerPerMicrosecond is constantly 10,000
        public const long TicksPerMicrosecond = 10;

        /// <summary>
        /// A divider used to scale time for waiting
        /// </summary>
        public const long Divider = TimeSpan.TicksPerMillisecond / TicksPerMicrosecond;

        static bool m_Disposed;

        /// <summary>
        /// The socket we use to keep track of time
        /// </summary>
        static Socket m_Socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

        /// <summary>
        /// The memory we give to the socket for events which should not occur
        /// </summary>
        static SocketAsyncEventArgs m_SocketMemory = new SocketAsyncEventArgs();

        public static DateTime LocalTime { get { return new DateTime(Environment.TickCount * TimeSpan.TicksPerMillisecond); } }

        public static DateTime UniversalTime { get { return LocalTime.ToUniversalTime(); } }

        /// <summary>
        /// Handles the creation of resources used to provide the μSleep method.
        /// </summary>
        static μTimer()
        {
            try
            {
                //Listen on the Loopback adapter on the specified port
                m_Socket.Bind(new System.Net.IPEndPoint(System.Net.IPAddress.Loopback, Port));

                //Only for 1 client
                m_Socket.Listen(1);

                //Assign an event now because in Begin process we will not call it if the even will not raise
                m_SocketMemory.Completed += BeginProcess;

#if(!MF)

                //If the SocketAsyncEventArgs will not raise it's own event we will call it now
                if (!m_Socket.AcceptAsync(m_SocketMemory))
                {
                    BeginProcess(typeof(μTimer), m_SocketMemory);
                }
#else
                new Thread(()=> BeginProcess(this, null)).Start();
#endif
            }
            catch
            {
                throw;
            }
        }

        /// <summary>
        /// Handles processing on the master time socket.
        /// This should never occcur.
        /// </summary>
        /// <param name="sender">The sender of the event</param>
        /// <param name="e">The SocketAsyncEventArgs from the event</param>
        /// <remarks>
        /// No one should connect... Ever.. (This is not a signaling implementation)
        /// </remarks>
#if(!MF)
        static void BeginProcess(object sender, SocketAsyncEventArgs e)
        {
#else 
        static void BeginProcess(object sender, object args e)
        {
            while(!m_Disposed)
            {  
                try
                { 
                    Socket dontCare = m_Socket.Accept(); dontCare.Dispose(); 
                    throw new System.InvalidProgramException("A Connection to the system was made by a unauthorized means."); 
                } 
                catch { throw; } 
            }
#endif
            if (!m_Disposed && e.LastOperation == SocketAsyncOperation.Connect)
            {
                try
                {
                    throw new System.InvalidProgramException("A Connection to the system was made by a unauthorized means.");
                }
                finally
                {
                    if (e.AcceptSocket != null)e.AcceptSocket.Dispose();
                }
            }
        }

        /// <summary>
        /// Performs a sleep using a method engineered by Julius Friedman (juliusfriedman@gmail.com)
        /// </summary>
        /// <param name="amount">The amount of time to Sleep</param>
        public static void μSleep(TimeSpan amount)
        {
            //Sample the system clock            
            DateTime now = μTimer.UniversalTime, then = μTimer.UniversalTime;
            TimeSpan waited = now - then;
            //If cpu time is not fast enough to accomadate then you are in bigger trouble then you know
            if (waited > amount) return;
            else System.Threading.Thread.Sleep(amount - waited); //A normal sleep with an amount less that 1 but greater than 0 Millisecond will not switch
            waited = now - then;//Waste cycles and calculate time waited in ticks again
            if (waited > amount) return;
            else unchecked
                {
                    //Scale time, basis of theory is we shouldn't be able to read from a socket in Accept mode 
                    //and it should take more time than a 1000th of the time we need
                    if (m_Socket.WaitRead(((int)((amount.Ticks - waited.Ticks / TicksPerMicrosecond) / Divider))))
                    {
                        //We didn't sleep
                        //Sample the system clock
                        then = μTimer.UniversalTime;
                        //Calculate waited
                        //Subtract time already waited from amount
                        amount -= waited;
                        //Waited set to now - then to determine wait
                        waited = now - then;
                        //return or utilize rest of slice sleeping
                        if (waited > amount) return;
                        else System.Threading.Thread.Sleep(amount - waited);
                    }
                }
        }

        /// <summary>
        /// Performs a sleep using a method engineered by Julius Friedman (juliusfriedman@gmail.com)
        /// </summary>
        /// <param name="amount">The amount of time to Sleep in microseconds(μs) </param>
        public static void μSleep(int amount) { μTimer.μSleep(TimeSpan.FromMilliseconds(amount * TimeSpan.TicksPerMillisecond)); }

        #endregion

        void IDisposable.Dispose()
        {
            m_Disposed = true;
            if (m_Socket != null)
            {
                m_Socket.Dispose();
                m_Socket = null;
            } 
        }
    }

    #endregion

Here is the testing code:

I even updated it to show that the StopWatch verifies that my method sleeps for under 1 μs.

  • Test Failed! (A Result of the writenotice function, take only note of the color which will be "DarkGreen" 
  • Exception.Message: StopWatch Elapsed during µSleep = 00:00:00.0000043
  • µTimer Took: 00:00:00
  • PerformanceCounter Took: 00:00:00
  • StopWatch Took 00:00:00.0000006
  • Test Passed! (Because my method was faster) 
  • Test Passed! (Because my method was faster) 
  • Press (W) to run again, (D) to debug or any other key to continue. (Press D or Q) 
  • 0 Failures, 7778 Successes (Because the CPU Has to warm up the first executions I believe)  
  • ......  
  • Sleep slept 0 µs. Sleep slept 43 Ticks. 1 Ops (Proof) 
  • 46533553 00:00:00.0150008
  • Sleep slept 0 µs. Sleep slept 46 Ticks. 1 Ops  (Proof) 
  • 999 Environment.TickCount: 46533569, delta: 16, 181 * (average TimeSlice Information) 
  • Min Delta: 15, Max Delta: 32.    
  • Sum Delta: 15803 / Num Delta: 1000 = Average Delta = 15.803000000 (Average TimeSlice time)  
  • (This proves the Envrionment.TickCount is too SLOW!!!)    

I have also included test code to measure the tick count!

The explanation for these results in short is as follows:

Just as when you offload work to the GPU this method offloads work to your NIC processor.

The network stack in your OS sets up an event in to perform this action by setting up an interrupt.

Then when the interrupter make the interrupt and the OS Completes the operation the interrupter is resumed back at the position where he interrupted the code giving you the precision you desire!

The nest steps would be WaitHandle derived implementation which overloads the defaults and gives Microsecond Precision which also add the ability to notify other threads as well!

Until that time you can find the code below!

static void RunTest(Action test, int count = 1)
{
    System.Console.Clear();
    Console.BackgroundColor = ConsoleColor.Blue;
    Console.WriteLine("About to run test: " + test.Method.Name);
    Console.WriteLine("Press Q to skip or any other key to continue.");
    Console.BackgroundColor = ConsoleColor.Black;
    if (Console.ReadKey().Key == ConsoleKey.Q) return;
    else
    {
        Dictionary<int, Exception> log = null;

        int run = count, failures = 0, successes = 0; bool multipleTests = count > 0;

        if (multipleTests) log = new Dictionary<int, Exception>();

    Test:
        try
        {
            System.Threading.Interlocked.Decrement(ref run);
            test();
            writeSuccess(multipleTests);
            System.Threading.Interlocked.Increment(ref successes);
        }
        catch (Exception ex)
        {
            System.Threading.Interlocked.Increment(ref failures);
            writeNotice(ex);
            if (multipleTests)
            {
                log.Add(run, ex);
                System.Threading.Thread.Yield();
            }
        }
        
        if (run >= 0) goto Test;
        else if (multipleTests)
        {
            if (failures > successes) writeNotice(new Exception("More Failures then Successes"));
            else writeSuccess(false, failures + " Failures, " + successes + " Successes");
        }

        ConsoleKey input = Console.ReadKey().Key;

        if (input == ConsoleKey.W) goto Test;
        else if (input == ConsoleKey.D) System.Diagnostics.Debugger.Break();
    }
}

[System.Runtime.CompilerServices.MethodImplAttribute(
         System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
static void writeNotice(Exception ex, ConsoleColor color = ConsoleColor.Red, bool pressStatement = true)
{
    ConsoleColor swap = Console.BackgroundColor;
    Console.BackgroundColor = color;
    Console.WriteLine("Test Failed!");
    Console.WriteLine("Exception.Message: " + ex.Message);
    if(pressStatement) Console.WriteLine("Press (W) to try again or any other key to continue.");
    Console.BackgroundColor = swap;
}

[System.Runtime.CompilerServices.MethodImplAttribute(
        System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
static void writeSuccess(bool auto = true, string message = null, ConsoleColor? color = null)
{
    ConsoleColor swap = Console.BackgroundColor;
    if (color.HasValue) Console.BackgroundColor = color.Value;
    else Console.BackgroundColor = ConsoleColor.Green;
    Console.WriteLine("Test Passed!");
    if (!auto) Console.WriteLine("Press (W) to run again, (D) to debug or any other key to continue.");
    if (!string.IsNullOrWhiteSpace(message)) Console.WriteLine(message);
    Console.BackgroundColor = swap;
}

static void TestEnvironmentTickCount()
        {
            int mindelta = int.MaxValue;
            int maxdelta = int.MinValue;
            long sumdelta = 0;
            long numdelta = 0;
            System.Diagnostics.Stopwatch w = new System.Diagnostics.Stopwatch();
            for (int i = 0; i < 1000; i++)
            {
                int d1 = Environment.TickCount;
                int d2 = d1;
                int sameval = 0;
                DateTime now = DateTime.UtcNow;
                uint ops = 0;
                while ((d2 = Environment.TickCount) == d1)
                {
                    ops = 0;
                    w.Reset();
                    w.Start();
                    DateTime then = DateTime.UtcNow;
                    Media.Common.μTimer.μSleep(TimeSpan.FromTicks((TimeSpan.TicksPerMillisecond / Media.Common.μTimer.TicksPerMicrosecond / Media.Common.μTimer.Divider)));
                    w.Stop();
                    ++sameval;
                    ++ops;
                    System.Threading.Thread.Sleep(0);
                    Console.WriteLine(Environment.TickCount + " " + (then - now));
                    Console.WriteLine("Sleep slept {0} µs. Sleep slept {1} Ticks. {2} Ops", (w.Elapsed.Ticks / ( TimeSpan.TicksPerMillisecond / Media.Common.μTimer.TicksPerMicrosecond)), w.Elapsed.Ticks, ops);
                    
                }                
                int delta = d2 - d1;
                mindelta = Math.Min(delta, mindelta);
                maxdelta = Math.Max(delta, maxdelta);
                sumdelta += delta;
                numdelta++;
                Console.WriteLine("{3:D3} Environment.TickCount: {0}, delta: {1}, {2} *", d2, delta, sameval, i);
            }
            double avgdelta = ((double)sumdelta) / ((double)numdelta);
            Console.WriteLine("Min Delta: {0}, Max Delta: {1}.", mindelta, maxdelta);
            Console.WriteLine("Sum Delta: {0} / Num Delta: {1} = Average Delta = {2:F9}", sumdelta, numdelta, avgdelta);
        }

Called like this:

public static void Main(string[] args)
{
    RunTest(TimerTest, 7777);//Address article post
    tickThread.Abort();
    tickThread = null;
} 

Even when using just the Performance counters there are some failures as you might come to expect, however in all cases I find my method is faster then the platform invocation and the counters.

Let me know if I am too high on my horse to see the big picture here or if I actually achieved something which others may find useful!

I updated the test code and it shows how to use a StopWatch to wait also.. it seems that is NOT faster because it uses the performance counters mine already beats...

The reason for this in short, is that my code uses IOCompletionPorts under the hood in Windows and on Unix it is using system calls.

See MSDN, tutorialspoint.

I am also confirming this working in the MicroFx as well as other places you may not expect e.g., Java et al and depends on how the underlying implementation provides the Poll method.

Here you will find the Guidelines for providing Multimedia Time Support, you can clearly see that it has a lengthy requirements list! 

On of the key points being: "Chipset vendors should implement an HPET to comply with Intels "IA-PC HPET (High Precision Event Timers) Specification" and this code is strengthened on the reliance that all NIC processors can handle this requirement quite easily!

Some will say this is a hack and a shortcut but in all honesty, is there anything really wrong with this? 

If you need the 'WaitRead' Method code you can check out Manged Media Aggregation which will be releasing this code very shortly along with a bunch of other goodies! 

Until then use Poll with the same value as I do in 'WaitRead' and SelectMode.Read  

Regards, 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

jfriedman
Software Developer (Senior) ASTI Transportation Inc.
United States United States
Livin in a lonely world, caught the midnight train going anywhere... Only thing is it was a runaway train... and it ain't ever goin back...

v//

You may also be interested in...

Comments and Discussions

 
GeneralSocket.Poll is more precise then Thread.Sleep only if Platform Timer Resolution is increased from default. Pin
lirco11-Sep-14 10:20
memberlirco11-Sep-14 10:20 
GeneralRe: Socket.Poll is more precise then Thread.Sleep only if Platform Timer Resolution is increased from default. Pin
jfriedman11-Sep-14 12:03
professionaljfriedman11-Sep-14 12:03 
GeneralMy vote of 1 Pin
Member 1103737526-Aug-14 4:23
memberMember 1103737526-Aug-14 4:23 
GeneralRe: My vote of 1 Pin
jfriedman26-Aug-14 4:29
professionaljfriedman26-Aug-14 4:29 
GeneralMy vote of 1 Pin
Member 1025864422-Jun-14 12:49
memberMember 1025864422-Jun-14 12:49 
GeneralRe: My vote of 1 Pin
jfriedman22-Jun-14 13:48
professionaljfriedman22-Jun-14 13:48 
GeneralMy vote of 1 Pin
phil.o6-Jun-14 10:20
professionalphil.o6-Jun-14 10:20 
GeneralRe: My vote of 1 Pin
jfriedman7-Jun-14 3:48
professionaljfriedman7-Jun-14 3:48 
GeneralRe: My vote of 1 Pin
_groo_10-Jun-14 5:57
member_groo_10-Jun-14 5:57 
GeneralRe: My vote of 1 Pin
jfriedman10-Jun-14 6:06
professionaljfriedman10-Jun-14 6:06 
Bug[My vote of 1] You are not measuring the elapsed time correctly Pin
_groo_6-May-14 21:56
member_groo_6-May-14 21:56 
GeneralRe: [My vote of 1] You are not measuring the elapsed time correctly Pin
jfriedman7-May-14 6:08
professionaljfriedman7-May-14 6:08 
GeneralMy vote of 1 Pin
Nchantim8-Jan-14 5:57
memberNchantim8-Jan-14 5:57 
GeneralRe: My vote of 1 Pin
jfriedman16-Jan-14 19:02
professionaljfriedman16-Jan-14 19:02 
QuestionTried it, but it doesn't seem accurate Pin
_groo_16-Nov-13 3:18
member_groo_16-Nov-13 3:18 
AnswerRe: Tried it, but it doesn't seem accurate Pin
jfriedman16-Jan-14 19:15
professionaljfriedman16-Jan-14 19:15 
GeneralRe: Tried it, but it doesn't seem accurate Pin
_groo_18-Apr-14 15:28
member_groo_18-Apr-14 15:28 
GeneralRe: Tried it, but it doesn't seem accurate Pin
jfriedman30-Apr-14 15:30
professionaljfriedman30-Apr-14 15:30 
GeneralRe: Tried it, but it doesn't seem accurate Pin
_groo_6-May-14 2:51
member_groo_6-May-14 2:51 
GeneralRe: Tried it, but it doesn't seem accurate Pin
jfriedman6-May-14 2:58
professionaljfriedman6-May-14 2:58 
GeneralRe: Tried it, but it doesn't seem accurate Pin
_groo_6-May-14 6:35
member_groo_6-May-14 6:35 
GeneralRe: Tried it, but it doesn't seem accurate Pin
jfriedman6-May-14 6:53
professionaljfriedman6-May-14 6:53 
QuestionHow to calculate the sleep time? Pin
BlackDogSpark12-Jun-13 14:41
memberBlackDogSpark12-Jun-13 14:41 
AnswerRe: How to calculate the sleep time? Pin
jfriedman5-Jul-13 7:43
professionaljfriedman5-Jul-13 7:43 
QuestionI'm confused. How to use this to replace System.Timers.Timer loop? Pin
BlackDogSpark10-Jun-13 9:13
memberBlackDogSpark10-Jun-13 9:13 
AnswerRe: I'm confused. How to use this to replace System.Timers.Timer loop? Pin
jfriedman10-Jun-13 10:33
professionaljfriedman10-Jun-13 10:33 
GeneralRe: I'm confused. How to use this to replace System.Timers.Timer loop? Pin
BlackDogSpark10-Jun-13 11:04
memberBlackDogSpark10-Jun-13 11:04 
GeneralRe: I'm confused. How to use this to replace System.Timers.Timer loop? Pin
jfriedman5-Jul-13 7:45
professionaljfriedman5-Jul-13 7:45 
QuestionWhere to find Socket.WaitRead()? Pin
Nils-Erik Thorén22-Apr-13 4:00
memberNils-Erik Thorén22-Apr-13 4:00 
AnswerRe: Where to find Socket.WaitRead()? Pin
jfriedman16-Jan-14 19:15
professionaljfriedman16-Jan-14 19:15 
GeneralMy vote of 5 Pin
dusty_dex11-Apr-13 11:32
memberdusty_dex11-Apr-13 11:32 
GeneralRe: My vote of 5 Pin
jfriedman11-Apr-13 11:39
memberjfriedman11-Apr-13 11:39 
QuestionMaybe I read through this a bit fast, but Pin
Espen Harlinn11-Apr-13 2:45
mvpEspen Harlinn11-Apr-13 2:45 
AnswerRe: Maybe I read through this a bit fast, but Pin
jfriedman11-Apr-13 6:13
memberjfriedman11-Apr-13 6:13 
GeneralRe: Maybe I read through this a bit fast, but Pin
millka11-Apr-13 8:12
membermillka11-Apr-13 8:12 
GeneralRe: Maybe I read through this a bit fast, but Pin
millka11-Apr-13 8:56
membermillka11-Apr-13 8:56 
GeneralRe: Maybe I read through this a bit fast, but Pin
jfriedman11-Apr-13 9:00
memberjfriedman11-Apr-13 9:00 
GeneralRe: Maybe I read through this a bit fast, but Pin
Espen Harlinn11-Apr-13 11:16
mvpEspen Harlinn11-Apr-13 11:16 
GeneralRe: Maybe I read through this a bit fast, but Pin
jfriedman11-Apr-13 11:40
memberjfriedman11-Apr-13 11:40 
QuestionThere is a problem.... Pin
Daniel Rozsar11-Apr-13 1:22
memberDaniel Rozsar11-Apr-13 1:22 
AnswerRe: There is a problem.... Pin
jfriedman11-Apr-13 6:55
memberjfriedman11-Apr-13 6:55 
QuestionNice idea, but it doesn't work Pin
millka5-Apr-13 14:22
membermillka5-Apr-13 14:22 
AnswerRe: Nice idea, but it doesn't work Pin
jfriedman6-Apr-13 9:46
memberjfriedman6-Apr-13 9:46 
General.. still doesn't work .. Pin
millka6-Apr-13 15:18
membermillka6-Apr-13 15:18 
GeneralRe: .. still doesn't work .. Pin
jfriedman6-Apr-13 16:07
memberjfriedman6-Apr-13 16:07 
GeneralRe: .. still doesn't work .. Pin
jfriedman7-Apr-13 15:41
memberjfriedman7-Apr-13 15:41 
GeneralRe: .. still doesn't work .. Pin
millka9-Apr-13 9:32
membermillka9-Apr-13 9:32 
GeneralRe: .. still doesn't work .. Pin
jfriedman10-Apr-13 14:19
memberjfriedman10-Apr-13 14:19 
GeneralRe: .. still doesn't work .. and here is why .. PinPopular
millka11-Apr-13 3:13
membermillka11-Apr-13 3:13 
Hi Julius,

i dont believe - i measure .. Wink | ;)

Your measurement code ist still wrong and therefore leads to wrong results. Let me explain:

In your latest test code, you measure the duration by calling Media.Common.µTimer.UniversalTime, which is implemented as:

public static DateTime LocalTime { get { return new DateTime(Environment.TickCount); } }
public static DateTime UniversalTime { get { return LocalTime.ToUniversalTime(); } }

So the new measurement is based on Enviroment.TickCount, which returns the number of milliseconds since the last start of Windows.
I think you assume, that Environment.TickCount returns increasing values with a step width of 1 (millisecond).
But that is not what is really happening ..

Lets see what actual values Environment.TickCount returns:

static void TestEnvironmentTickCount () {
  int mindelta = int.MaxValue;
  int maxdelta = int.MinValue;
  long sumdelta = 0;
  long numdelta = 0;
  for (int i = 0; i < 1000; i++) {
    int d1 = Environment.TickCount;
    int d2 = d1;
    int sameval = 0;
    while ((d2 = Environment.TickCount) == d1) sameval++;
    int delta = d2 - d1;
    mindelta = Math.Min (delta, mindelta);
    maxdelta = Math.Max (delta, maxdelta);
    sumdelta += delta;
    numdelta++;
    Console.WriteLine ("{3:D3} Environment.TickCount: {0}, delta: {1}, {2} *", d2, delta, sameval, i);
  }
  double avgdelta = ((double) sumdelta) / ((double) numdelta);
  Console.WriteLine ("Min Delta: {0}, Max Delta: {1}.", mindelta, maxdelta);
  Console.WriteLine ("Sum Delta: {0} / Num Delta: {1} = Average Delta = {2:F9}", sumdelta, numdelta, avgdelta);
  Console.ReadKey ();
}

For statistical purposes, we have a min and max delta value and a sum and counter of delta values.
Inside the outer for loop, the test works like this:
We get a time stamp d1 from Environment.TickCount.
In the inner while loop, we get another time stamp d2 from Environment.TickCount.
If this second time stamp d2 is equal to the first d1, we stay in the while loop and increase sameval by one, because we got the same return value from Environment.TickCount as before. The difference of s2 - d1 would be 0, since d2 is equal to d1.
As soon as Environment.TickCount returns a value d2 different from d1, we fall out of the while loop.
Now we calculate the difference 'delta' between the two "clock readings" d2 and d1.
Since we fell out of the loop, d2 must be not equal to d1, the difference delta must be not 0.
In mindelta and maxdelta we keep track of the minimum and maximum difference we found.
To calculate an average later, we sum and count the difference 'delta'.
Next we print the "clock reading" from Environment.TickCount and the difference 'delta' and how many times we got the same return value.
So the counter 'sameval' tells us how many times we "looked at the clock" without the "clocks watch hand moving".
The difference 'delta' tells us how far the "clocks watch hand moved" when it finally moved.
We repeat the inner test a thousand times.
Then we calculate the average delta by dividing the sum by the counter.
At last we print min and max delta, sum and counter and average.

So what values do we expect ?

Since Environment.TickCount is a property of type int, returning milliseconds, it should return increasing values with a difference of exactly 1.
The the test code should print a minimum value of 1 ms, a maximum value of 1 ms, a sum of 1000 ms, and a counter value of 1, and an average value of 1 ms.

But what values to we actually get ?

995 Environment.TickCount: 52388718, delta: 15, 1613995 *
996 Environment.TickCount: 52388734, delta: 16, 1608911 *
997 Environment.TickCount: 52388750, delta: 16, 1590424 *
998 Environment.TickCount: 52388765, delta: 15, 1603644 *
999 Environment.TickCount: 52388781, delta: 16, 1603553 *
Min Delta: 15, Max Delta: 16.
Sum Delta: 15625 / Num Delta: 1000 = Average Delta = 15.625000000

So in the 998th run, d1 was 52388750 before the while loop. In the while loop we "looked at the clock" 1603644 times without the "clock moving". When it finally moved, it jumped from 52388750 to 52388765. The difference 'delta' is 15 ms.

But Environment.TickCount never ever returned the values between 52388750 to 52388765. In theory it could, but in reality it does not. Because Windows unfortunately updates Enviroment.TickCount just once per time slice.

For 1000 tests, the average is 15.625 ms, a frequency of 64 Hz.

Why is that a problem ?

If the accuracy (= step width) of the clock function used to measure something is 15.625 ms, your clock readings will always be an integral multiple of those 15.625 ms. Any difference between two clock readings will always be an integral multiple of those 15.625 ms too. That means you cant measure a single duration smaller than 15.625 ms with a clock (function) ticking at 15.625 ms or 64 Hz.

Assume a time slice has just begun and the Enviroment.TickCount just returned a different value than before, lets say it returned 1,000,000 (ms) = 1,000,000,000 µs. Now you call your high resolution sleep function. The CPU executes all the instructions of your sleep function and the select.Poll call itself. If select.Poll itself had to sleep for e.g. 10 µs, all this might take maybe 12 µs or something. Now the time would be 1,000,000,012 µs, 12 µs later. To measure the duration, you call Environment.TickCount and it will return 1,000,000 ms again. When you calculate the difference, it will be 0. It will look like sleep didnt sleep at all.

Now assume your sleep call is called 5 µs before the end of a time slice, e.g. at Enviroment.TickCount = 2,000,000 ms (= 2,000,000,000 µs). When Sleep comes back 12 µs later, were already 7 µs (= 12 - 5) into the next time slice, so Enviroment.TickCount will return 2,000,015 ms, because Windows increased TickCount by 15.625 µs when it switched from the previous time slice to the current time slice. Now you measure a difference of 15 ms = 15625 µs. It will look like your sleep function took a thousand times more than it should.

That means using Environment.TickCount is the wrong tool to measure, because it jumps in steps of 15.625 ms.
You simply cannot reliably measure a small duration (e.g. 12 µs) with large clock ticks (e.g. 15625 µs), just like you cannot measure a length of 12 millimeters with a ruler that has only tick marks every 15 meters.

In your test code, you read the clock before (now) and after (then) the call to your sleep function.

//Use Poll Method
now = Media.Common.μTimer.UniversalTime;
 
//Sleep using my method
Media.Common.μTimer.μSleep(delay);
 
//Sample the clock
then = Media.Common.μTimer.UniversalTime;
 
//Calculate the result
TimeSpan μActually = then - now;

Then you try to calculate the duration 'µActually' as the difference 'then' - 'now'. As I have shown above, 'then' will either be exactly the same value as 'now' (because both clock readings happen within the same time slice), or 'then' will 15 ms larger than 'now' (if 'now' was in the previous time slice, and 'then' in the next time slice). So your calculated difference 'µActually' will be either too small (= 0 µs) or way too large (= 15625 µs).

I hope you can see and understand the problem with your measurement method now ..

And then there are a few minor problems with your new measure methods:

public static DateTime LocalTime { get { return new DateTime(Environment.TickCount); } }
public static DateTime UniversalTime { get { return LocalTime.ToUniversalTime(); } }

Environment.TickCount returns ticks of 1 millisecond, but the DateTime constructor expects ticks of 100 nanoseconds. You neet to multiply by 10000:

public static DateTime LocalTime { get { return new DateTime(10000 * Environment.TickCount); } }

The origin (zero point) of Enviroment.TickCount is the last boot, which is different from DateTime's origin. An offset would be needed. It would be better to use TimeSpan instead. Then you dont need to worry about time zones, too.


How can the real problem (Environment.TickCount too coarse) be solved ?

Environment.TickCount is bad for two reasons:
- its precision (smallest unit) is only milliseconds
- it jump in 15625 µs increments

DateTime.Ticks is only slightly better:
- its precision (smallest unit) is in ticks of 100 nanoseconds (= 0.1 µs)
- it jump in 15625 µs increments

System.Diagnostics.Stopwatch.Elapsed.Ticks is much better:
- its precision is in ticks of 100 nanoseconds (= 0.1 µs)
- it increments independently of the schedulers time slice
The smallest timespan I could measure on my 3.2 GHz machine is 0.5 µs

To measure reliable times, you need to use Stopwatch.
However, dont mix up Stopwatch.Elapsed.Ticks and Stopwatch.ElapsedTicks !
- Stopwatch.Elapsed.Ticks (the TimeSpan.Ticks property of the Stopwatch.Elapsed property) uses 0.1 µs ticks.
- Stopwatch.ElapsedTicks uses a CPU frequency dependent unit.

When you change your test code to use Stopwatch, you will measure that your sleep function takes a duration of 100 µs for parameters below 100 µs, and a duration of 15625 µs for parameters above 100 µs.

The reason, again, is the Windows schedulers time slice.

This time slice can be decreased from 15625 µs (= 64 Hz) to 976 µs (= 1024 Hz) by calling timeBeginPeriod(1). See my previous replies.
GeneralRe: .. still doesn't work .. and here is why .. Pin
jfriedman11-Apr-13 6:26
memberjfriedman11-Apr-13 6:26 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web03 | 2.8.160208.1 | Last Updated 12 Apr 2013
Article Copyright 2013 by jfriedman
Everything else Copyright © CodeProject, 1999-2016
Layout: fixed | fluid