Accuracy of Millisecs()

BlitzMax Forums/BlitzMax Programming/Accuracy of Millisecs()

As far as you know, is Millisecs() accurate on *all* PCs? I appreciate that when you read it, it's rounded to the nearest millisec (wonder if it uses bankers rounding, normal rounding, or Floor or Ceil?). Anyway, I wondered if sometimes it can return values that are out by quite a few milliseconds on some PCs, perhaps if other tasks are interfereing. Anyone see anything like this posted before? I remember seeing someone say something about the accuracy of Millisecs() and wondered what they meant.

Its the same on everyone's PC.

I appreciate that when you read it, it's rounded to the nearest millisec (wonder if it uses bankers rounding, normal rounding, or Floor or Ceil?).
Who cares? Its 1/1000th of a second. You can't comprehend such a small amount of time.

Time is time after all unless your name is DR Who.

Today, typical CPUs run in excess of 3GHz, so you'd think that 1ns accuracy shouldn't be that big a deal - it's three clock cycles right? But remember that CPU pipelines tend to run pretty deep these days. So while your processor might be able to execute instructions at a rate of several per nanosecond, that doesn't mean that any individual instruction necessarily executes in under a nanosecond.
http://www.interact-sw.co.uk/iangblog/2005/03/31/nanoperf

A better path is to use Win32's special high-resolution timers that span a pair of functions: QueryPerformanceCount() and QueryPerformanceFrequency(). On Intel-based systems the functions rely on a counter built into Pentium chips since the Pentium II generation. When an Intel-based system is started up, a 64-bit register keeps track of elapsed clock ticks. This counter provides an extremely high-resolution timing device.
http://www.devx.com/SummitDays/Article/16293/1411/pdo/29FD2B1375EBAC824CE7A420FAC318F4:3835

Hmm that hi-res thing sounds interesting, but really it would need to work on Celerons and AMDs as well...

Umn, try it?

I don't have non Pentiums. Seems that to use it properly you have to get a totally accurate figure for the speed of the chip first.

Millisecs is accurate down to the millisecond, for any crystal or square pulse generator running close to 10 KHz (10 pulses per millisecond). I don't care how long your CPU pipeline is, it's not going to interfere with millisecond accuracy.

you have to get a totally accurate figure for the speed of the chip first.


And as even EA and other are not capable of that, I would not try it ... (Sims 2 and any other game having issues with HT / dualcore suffers exactly of this problem because "speed" is relative when you have Cool'n'Quiet / SpeedStep technology)

I've written a Profiler-module (based on some code I found in the code archives) using QueryPerformanceCount. It runs well on my system (Athlon XP). It's not in the archives yet(no time to really finish it and check for bugs), but feel free to use it or copy code from it:

strict
rem
bbdoc: Module bt.profiler
about: Highspeed-Measurement (Win32 only)
<table>
<tr><th>Platform</th><th>Win32</th><th>X-Win</th><th>MacOS</th></tr>
<tr><td></td><td>Yes</td><td>No</td><td>No</td></tr>
</table>
end rem

Module bt.profiler
ModuleInfo "Version: 1.01"
ModuleInfo "Author: Jake"
ModuleInfo "License: Freeware"

moduleinfo "History: 1.01 Release"
moduleinfo "         Added Avg-% (FrameTime)"
ModuleInfo "History: 1.00 Release"

Import brl.system
Import brl.math
Import brl.linkedlist
import brl.D3D7Max2D
Import brl.max2d
Import brl.retro

Extern "Win32"
       Function QueryPerformanceFrequency:Int(freqency:Long Ptr)
       Function QueryPerformanceCounter:Int(frequency:Long Ptr)
End Extern

rem
bbdoc: Profiler class

Sample:

myProfiler:TBTProfiler=TBTProfiler (250)			'display-update every 250ms		
pmain:TBTProfItem=myProfiler.AddItem("Mainloop")	'create Item
ptest:TBTProfItem=myProfiler.AddItem("Stop my time")	'create nested Item (see below)

while not keydown(KEY_ESCAPE)
	pmain.Start()			  ' start measure
		ptest.Start()
			delay(1000)			
		ptest.Stop()		  ' stop measure

	myProfiler.Profile()	  ' calculate results
	myProfiler.Display(10,10) ' display results on screen at Point(10,10)
	Flip
	pmain.Stop()
wend

endrem
Type TBTProfiler
	Global _Items		:TList
	Field enabled		:Int	=	True
	Field freq			:Long
	Field apioverhead	:Long
	Field onlineupdate	:Int	'update interval
	Field onlinetime	:Int	'time of next update
	Field onlinelast	:Int
	field frametime		:double	'max available time (e.g. 60Hz=14.666)
	
	Function Create:TBTProfiler(onlineupdate%=500,maxframetime!=0)
		Local me:TBTProfiler=New TBTProfiler
		Local t1:Long
		Local t2:Long
			_Items=New TList
			QueryPerformanceCounter(VarPtr(t1))
			QueryPerformanceCounter(VarPtr(t2))
			QueryPerformanceFrequency(VarPtr (me.freq))
			me.apioverhead=t2-t1
			me.onlineupdate=onlineupdate
			me.onlinetime=MilliSecs()+onlineupdate
			if maxframetime then me.SetFrameTime(maxframetime)
		Return (me)
	End Function
	
	Method SetFrameTime (max_time!)
		frametime=max_time
	End Method
	
    rem
    bbdoc: calculate results for all Profile-Items
    endrem
	Method Profile()
		If not enabled Return
		If onlinetime>MilliSecs() Return	
		onlinelast=onlinetime
		onlinetime:+onlineupdate
		For Local item:TBTProfItem=EachIn _Items
			item.Calc()
		Next
		
	End Method
    rem
    bbdoc: display results of Profile()
    endrem
	Method Display(x%,y%)
	
			Function DrawTextAll(str$, x, y)
			  If TD3D7Max2DDriver(_max2dDriver)
			    DrawText str$, x, y
			  else
			    GLDrawtext str$, x, y
			  endif
			End Function

		If not enabled Return
			DrawTextAll ("Item               | Avg (ms)| Min (ms)| Max (ms) |Avg (%)",x,y)
			y:+16
			DrawTextAll ("==========================================================",x,y)
			y:+16
		For Local item:TBTProfItem=EachIn _Items
			DrawTextAll (item.res,x,y)
			y:+16
		Next
		
	End Method
	rem
	bbdoc: add a Item
	end rem
	Method AddItem:TBTProfItem (name$)
		Return TBTProfItem.Create(Self,name)
	End Method
	

End Type
      
Type TBTProfItem
	Global _ItemLevel	:Int	
	
	Field profiler		:TBTProfiler
	Field _name			:String
	Field Level			:Int
	Field running		:Int
	Field tstart		:Long
	Field tstop			:Long
	Field Ticks			:Int
	Field taccu			:Float
	Field tmin			:Float
	Field tmax			:Float
	Field res			:String
	
	Function Create:TBTProfItem(profiler:TBTProfiler,name$)
		Local me:TBTProfItem=New TBTProfItem
			me.profiler=profiler
			me.profiler._Items.AddLast(me)
			me._name=name
			me.Ticks=1
			me.Calc()
		Return (me)
	End Function
	rem
	bbdoc: Start measure
	endrem
	Method Start()
		running=True
		Level=_ItemLevel
		_ItemLevel:+1
		QueryPerformanceCounter (VarPtr(tstart))
	End Method
	
	Method Calc()
		res=LSet(RSet(_name, _name.length+2*Level),19)+"|"
		res=res+RSet(Left(String.FromFloat(taccu/Ticks),5),8)+" |"
		res=res+RSet(Left(String.fromfloat(tmin),5),8)+" |"
		res=res+RSet(Left(String.fromfloat(tmax),5),8)+" |"
		res=res+RSet(Left(String.fromfloat(100*(taccu/Ticks) / Profiler.FrameTime),4),8)
		Reset()
	End Method
	rem
	bbdoc: Stop measure
	endrem
	Method Stop()
		QueryPerformanceCounter (VarPtr(tstop))
		Local this#=Float(tstop-tstart-profiler.apioverhead) / profiler.freq *1000
		tmin=min(tmin,this)
		tmax=max(tmax,this)
		taccu:+this
		Ticks:+1
		running=False
		_ItemLevel:-1
	End Method
	
	Method Reset()
		taccu=0
		tmin=9999
		tmax=0
		Ticks=0
	End Method
	
End Type 


At the instant that you call Millisecs() it should be accurate. That said, bare in mind that you can execute a fair amount of code WITHIN 1 millisecond. If you just read the value, and are only going to get updates from it ever millisecond, it's possible that it also just changed to the next millisecond the instant after you read it. Your code is not synchronized with the timer. So at the most you might be up to 1 millisecond off. It's not so much the accuracy of the timer which is always to the millisecond, but moreso the accuracy of your checking of the timer and catching it exactly when it changes.

I thought about this too when I was trying to measure the amount of remaining time left in a frame. Ideally each frame was going to last 16 milliseconds. Thing was, since it's possible to read a value just before it changes, I sort of had to reduce the number of available millisecs per frame by 1 to compensate, which reduced the amount of processing time available in the frame. If I didn't do this, the flip may possibly have not occured within the frame time and would've waited for the next vertical blank, wasting a whole frame and making things jerky.

So you only really need worry about having not checked the millisecs recently enough and the possibility that there is some delay between your checking it and your using it. The closer your code use of it is to the reading of it the better. That said, if you just read the value just *after* it changed, and you're waiting for a given future change, then you may end up with potentially up to a whole millisecond extra time, based on when you read it the first time and when you read it the second time. ie your code's execution resolution is higher than the resolution of the timer.

Thanks Jake.L and others for feedback.

AngelDaniel: Exactly, when you read the timer it could be at the start of a millisec or the end of one, and your code might think a millisec has passed by the next loop when really 0.01 of a millisec has passed.

"bare in mind" Freudian ;-)

Hmm. You could check for a millisecond value, then rapidly check until 2 millisecs have passed, because that'll probably mean 1.02 millisecs have passed. ;-)

why dont you wait for a millisecond to pass (the millisec value changing) before reading, that way you will test from the beginning of a millisecond and that will give you an 'accurate' result.

EdzUp: Yes that's true. It's not really actually an issue for me, I was just interested in if it's inaccurate by BIG amounts on some systems, perhaps when interrupted by other tasks.

The timer value itself should be accurate despite anything else that is going on because presumably the timer HAS to at some level be generated by the hardware itself which surely would be always processed internally regardless of any software running.

that's what I thought, but was just interested if ever say a request to the time could return duff values due to other threads interfering. I guess not.

Sub Nano Second timing, this is actually time in cpu cycles, you'll need to do a little math to calibrate it to time.

__int64 GetMachineCycleCount()
{      
   __int64 cycles;
   _asm rdtsc; // won't work on 486 or below - only pentium or above
   _asm lea ebx,cycles;
   _asm mov [ebx],eax;
   _asm mov [ebx+4],edx;
   return cycles;
}


groovy choovy, unfortunately to work out the time, you need to know the accurate CPU speed but it seems that getting that accurate ain't so easy...

Wrong, calculating the number of cycles per millisecond is easy:
int64 timethen = GetMachineCycleCount();
Sleep(1);
int64 timenow = GetMachineCycleCount();
int64 one_millisecond = timenow - timethen;
float one_nanosecond =  (float)((float)one_millisecond / (float)1000); // It's gonna be 1 cycle for 1 GHz


Of course, good one Tim!

question is, how does Sleep(1) determine when a millisec is up? If it just reads a value and waits until it's 1 higher, this could be only a fraction of a millisecond, or anything up to a whole millisecond. How does Delay(1) ensure that it stops for one millisec? Maybe delay 10 would be more accurate as the delay could be 9-10ms.

The closer to the speed of light your PC gets, the longer each millisec becomes.

you can simulate this by attaching your PC to a 12 meter rope and spinning it with the rotational velocity of 1,034,456.

I would try again but my rope broke.

Yeah I'm using Sleep(1) as a baseline, In the real world you might want to do it for 100ms to account for any small errors.

I assume this is to prevent any stuttering when msn etc pops up??

float one_microsecond =  (float)((float)one_millisecond / (float)1000); // It's gonna be 1 cycle for 1 GHz


Silly me, 1 nano second is one cycle - 1 microsecond is 1000 cycles on a 1ghz machine

I forgot to change the comment. :o)

I assume this is to prevent any stuttering when msn etc pops up??
nah, sorry. See the thread priority thread for info about that.