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
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")]
static extern int usleep (uint amount);
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);
public static void uSleep(TimeSpan amount) { μTimer.uSleep(((int)(amount.TotalMilliseconds * 1000))); }
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
const ushort Port = 7777;
public const long TicksPerMicrosecond = 10;
public const long Divider = TimeSpan.TicksPerMillisecond / TicksPerMicrosecond;
static bool m_Disposed;
static Socket m_Socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
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(); } }
static μTimer()
{
try
{
m_Socket.Bind(new System.Net.IPEndPoint(System.Net.IPAddress.Loopback, Port));
m_Socket.Listen(1);
m_SocketMemory.Completed += BeginProcess;
#if(!MF)
if (!m_Socket.AcceptAsync(m_SocketMemory))
{
BeginProcess(typeof(μTimer), m_SocketMemory);
}
#else
new Thread(()=> BeginProcess(this, null)).Start();
#endif
}
catch
{
throw;
}
}
#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();
}
}
}
public static void μSleep(TimeSpan amount)
{
DateTime now = μTimer.UniversalTime, then = μTimer.UniversalTime;
TimeSpan waited = now - then;
if (waited > amount) return;
else System.Threading.Thread.Sleep(amount - waited);
waited = now - then;
if (waited > amount) return;
else unchecked
{
if (m_Socket.WaitRead(((int)((amount.Ticks - waited.Ticks / TicksPerMicrosecond) / Divider))))
{
then = μTimer.UniversalTime;
amount -= waited;
waited = now - then;
if (waited > amount) return;
else System.Threading.Thread.Sleep(amount - waited);
}
}
}
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);
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 IOCompletionPort
s 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,