Click here to Skip to main content
Click here to Skip to main content

Tagged as

Go to top

Obtaining Microsecond Precision in .NET

, 11 Apr 2013
Rate this:
Please Sign up or sign in to vote.
Obtaining microsecond precision using .NET without Platform Invoke.
This is an old version of the currently published article.

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 = 1000;

    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 >

Here is the testing code:

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

  • Exception.Message: Timer Took: 00:00:00
  • PerformanceCounter Took: 00:00:00
  • StopWatch Elapsed during µSleep = 00:00:00.0000043

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;
    for (int i = 0; i < 1000; i++)
    {
        int d1 = Environment.TickCount;
        int d2 = d1;
        int sameval = 0;
        //while ((d2 = Environment.TickCount) == d1) sameval++;
        while ((d2 = Environment.TickCount) == d1) Media.Common.μTimer.μSleep(1);
        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);
}

static System.Threading.Thread tickThread;

public static void TimerTest()
{
    if (tickThread == null)
    {
        tickThread = new System.Threading.Thread(TestEnvironmentTickCount);
        tickThread.Priority = System.Threading.ThreadPriority.Highest;
        tickThread.SetApartmentState(System.Threading.ApartmentState.MTA);
        tickThread.Start();
    }
    TestTimer();
}

public static void TestTimer()
{
    //Always only 1 delay possible 1 μs
    TimeSpan delay = TimeSpan.FromTicks(TimeSpan.TicksPerMillisecond / 1000);
    
    //Make a StopWatch
    System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
    

    //Start at 0
    TimeSpan watch = TimeSpan.Zero;
    //Start, Stop as fast as we can
    sw.Start();
    sw.Stop();

    //Calculate elapsed
    watch = sw.Elapsed;

    //Sample system clock
    DateTime now = Media.Common.μTimer.UniversalTime;

    //Calculate when it should be when the sleep is done
    DateTime shouldBe = now + delay;

    //Use the performance counter method
    Media.Common.μTimer.uSleep(delay);

    //Sample the clock
    DateTime then = Media.Common.μTimer.UniversalTime;

    //Calculate the result
    TimeSpan pActually = then - now;

    //Use Poll Method
    now = Media.Common.μTimer.UniversalTime;

    //Calculate when it should be when the sleep is done
    shouldBe = now + delay;

    sw.Start();

    //Sleep using my method
    Media.Common.μTimer.μSleep(delay);

    sw.Stop();

    //Sample the clock
    then = Media.Common.μTimer.UniversalTime;

    //Calculate the result
    TimeSpan μActually = then - now;

    TimeSpan totalU = μActually - delay;

    TimeSpan totalP = μActually - pActually;

    TimeSpan totalS = μActually - watch;

    if (μActually > delay)
    {
        writeNotice(new Exception("μTimer Precision is not Correct!"), ConsoleColor.Red, false);
    }
    else if (pActually > delay)
    {
        writeNotice(new Exception("Performance Counter Precision is not Correct!"), ConsoleColor.Red, false);
    }
    else if (watch > delay)
    {
        writeNotice(new Exception("StopWatch Counter Precision is not Correct!"), ConsoleColor.Red, false);
    }
    else if (pActually < μActually)
    {
        writeNotice(new Exception("Performance Counter beat μTimer!"), ConsoleColor.Red, false);
    }
    else if (watch < μActually)
    {
        writeNotice(new Exception("StopWatch Counter beat μTimer!"), ConsoleColor.Red, false);
    }
    else
    {
        writeNotice(new Exception(("StopWatch Elapsed during μSleep = " + 
          sw.Elapsed + " μTimer Took: " + μActually + " PerformanceCounter Took: " + 
          pActually + "StopWatch Took " + watch)), ConsoleColor.DarkGreen, false);
    }            
}

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?

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//
Follow on   Twitter   Google+

Comments and Discussions


Discussions posted for the Published version of this article. Posting a message here will take you to the publicly available article in order to continue your conversation in public.
 
GeneralSocket.Poll is more precise then Thread.Sleep only if Platform Timer Resolution is increased from default. [modified] Pinmemberlirco11-Sep-14 9:20 
GeneralRe: Socket.Poll is more precise then Thread.Sleep only if Platform Timer Resolution is increased from default. Pinprofessionaljfriedman11-Sep-14 11:03 
GeneralMy vote of 1 PinmemberMember 1103737526-Aug-14 3:23 
GeneralRe: My vote of 1 Pinprofessionaljfriedman26-Aug-14 3:29 
GeneralMy vote of 1 PinmemberMember 1025864422-Jun-14 11:49 
GeneralRe: My vote of 1 Pinprofessionaljfriedman22-Jun-14 12:48 
GeneralMy vote of 1 Pinprofessionalphil.o6-Jun-14 9:20 
GeneralRe: My vote of 1 Pinprofessionaljfriedman7-Jun-14 2:48 
GeneralRe: My vote of 1 Pinmember_groo_10-Jun-14 4:57 
GeneralRe: My vote of 1 Pinprofessionaljfriedman10-Jun-14 5:06 
Bug[My vote of 1] You are not measuring the elapsed time correctly Pinmember_groo_6-May-14 20:56 
GeneralRe: [My vote of 1] You are not measuring the elapsed time correctly Pinprofessionaljfriedman7-May-14 5:08 
GeneralMy vote of 1 PinmemberNchantim8-Jan-14 4:57 
GeneralRe: My vote of 1 Pinprofessionaljfriedman16-Jan-14 18:02 
QuestionTried it, but it doesn't seem accurate [modified] Pinmember_groo_16-Nov-13 2:18 
AnswerRe: Tried it, but it doesn't seem accurate Pinprofessionaljfriedman16-Jan-14 18:15 
GeneralRe: Tried it, but it doesn't seem accurate Pinmember_groo_18-Apr-14 14:28 
GeneralRe: Tried it, but it doesn't seem accurate Pinprofessionaljfriedman30-Apr-14 14:30 
GeneralRe: Tried it, but it doesn't seem accurate Pinmember_groo_6-May-14 1:51 
GeneralRe: Tried it, but it doesn't seem accurate Pinprofessionaljfriedman6-May-14 1:58 
GeneralRe: Tried it, but it doesn't seem accurate Pinmember_groo_6-May-14 5:35 
GeneralRe: Tried it, but it doesn't seem accurate Pinprofessionaljfriedman6-May-14 5:53 
QuestionHow to calculate the sleep time? PinmemberBlackDogSpark12-Jun-13 13:41 
AnswerRe: How to calculate the sleep time? Pinprofessionaljfriedman5-Jul-13 6:43 
QuestionI'm confused. How to use this to replace System.Timers.Timer loop? PinmemberBlackDogSpark10-Jun-13 8:13 
AnswerRe: I'm confused. How to use this to replace System.Timers.Timer loop? Pinprofessionaljfriedman10-Jun-13 9:33 
GeneralRe: I'm confused. How to use this to replace System.Timers.Timer loop? PinmemberBlackDogSpark10-Jun-13 10:04 
GeneralRe: I'm confused. How to use this to replace System.Timers.Timer loop? Pinprofessionaljfriedman5-Jul-13 6:45 
QuestionWhere to find Socket.WaitRead()? PinmemberNils-Erik Thorén22-Apr-13 3:00 
AnswerRe: Where to find Socket.WaitRead()? Pinprofessionaljfriedman16-Jan-14 18:15 
GeneralMy vote of 5 Pinmemberdusty_dex11-Apr-13 10:32 
GeneralRe: My vote of 5 Pinmemberjfriedman11-Apr-13 10:39 
QuestionMaybe I read through this a bit fast, but PinmvpEspen Harlinn11-Apr-13 1:45 
AnswerRe: Maybe I read through this a bit fast, but Pinmemberjfriedman11-Apr-13 5:13 
GeneralRe: Maybe I read through this a bit fast, but Pinmembermillka11-Apr-13 7:12 
GeneralRe: Maybe I read through this a bit fast, but Pinmembermillka11-Apr-13 7:56 
GeneralRe: Maybe I read through this a bit fast, but [modified] Pinmemberjfriedman11-Apr-13 8:00 
GeneralRe: Maybe I read through this a bit fast, but PinmvpEspen Harlinn11-Apr-13 10:16 
GeneralRe: Maybe I read through this a bit fast, but Pinmemberjfriedman11-Apr-13 10:40 
QuestionThere is a problem.... PinmemberDaniel Rozsar11-Apr-13 0:22 
AnswerRe: There is a problem.... Pinmemberjfriedman11-Apr-13 5:55 
QuestionNice idea, but it doesn't work [modified] Pinmembermillka5-Apr-13 13:22 
AnswerRe: Nice idea, but it doesn't work Pinmemberjfriedman6-Apr-13 8:46 
General.. still doesn't work .. Pinmembermillka6-Apr-13 14:18 
GeneralRe: .. still doesn't work .. Pinmemberjfriedman6-Apr-13 15:07 
GeneralRe: .. still doesn't work .. Pinmemberjfriedman7-Apr-13 14:41 
GeneralRe: .. still doesn't work .. Pinmembermillka9-Apr-13 8:32 
GeneralRe: .. still doesn't work .. Pinmemberjfriedman10-Apr-13 13:19 
GeneralRe: .. still doesn't work .. and here is why .. Pinmembermillka11-Apr-13 2:13 
GeneralRe: .. still doesn't work .. and here is why .. Pinmemberjfriedman11-Apr-13 5:26 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    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 | Mobile
Web03 | 2.8.140926.1 | Last Updated 11 Apr 2013
Article Copyright 2013 by jfriedman
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid