Mon, 04 Mar 2024
The simple methods described here work with CPU having the CPUID (80486) and RDTSC (Pentium) instructions but are not meant to support CPU beyond the Intel P54C family because of the power management factor that could disrupt the measures.
However, it is still possible to disable the power management in those cases.
Help yourself to find out ;)
The code below is tailored for WATCOM C.
The processor monotonically increments the time-stamp counter MSR (Model Specific Register) every clock cycle and resets it to 0 whenever the processor is reset.
The code here after gives access to the rdtsc
instruction from the C code
through inline assembly so that we can read the CPU time stamp counter:
/* Fills the Time Stamp Counter. */
void x86_rdtsc(u64 __far *tsc);
#pragma aux x86_rdtsc = \
"rdtsc" \
"mov DWORD PTR [edi+4], edx" \
"mov DWORD PTR [edi], eax" \
parm [edi] \
modify exact [eax ebx ecx edx edi];
By simply waiting for 1 second, it's easy to retrieve the CPU frequency in Hz and then compute it in Mhz. The main drawback to this method is the 1 second delay you need to wait to query the CPU frequency.
u64 tsc0, tsc1;
x86_rdtsc(&tsc0);
sleep(1);
x86_rdtsc(&tsc1);
printf("clock frequency = %llu Hz\n", tsc1 - tsc0);
printf("clock frequency = %lu MHz\n", (tsc1 - tsc0)/1000000);
Derived from FREQUENC.ASM found in Intel Processor Identification and the CPUID Instruction.
I mainly removed the power-management registers (remember the introduction?) and rely on the 8254 PIT Channel 0 (System Time Counter / RTC) to code an optimized delay.
The 8254 Channel 0 System Time Counter is found at the BIOS data area address
[0040:006C]
. It is updated 18.2 times per seconds (e.g., every ~54.95ms),
which means that 5 intervals is about 275ms.
First we need to read the bios tick count at address [0040:006C]
([046C]
in
protected mode)
u32 x86_read_bios_rtc();
#pragma aux x86_read_bios_rtc = \
"mov edi, 046Ch" \
"mov eax, [edi]" \
value [eax] \
modify exact [eax edi];
In the code to query the frequency, we make sure we're counting from the very start of a new tick (first while loop).
u32 ticks = x86_read_bios_rtc(); /* read the RTC counter */
while(x86_read_bios_rtc() == ticks) /* in case we're in the middle of a tick */
;
x86_rdtsc(&tsc0); /* read and save the initial TSC */
ticks += (5 + 1); /* now wait for the delay */
while(x86_read_bios_rtc() != ticks)
;
x86_rdtsc(&tsc1); /* read and save the new TSC */
cpu_performance_frequency = (tsc1 - tsc0) * (18.2f / 5);
Thanks to this method, the query latency is reduced from ~1000ms to ~275ms
(+/- 54ms because of the first while
loop to wait for the end of a started
RTC tick)
The previous method relies on the 8254 (PIT - Programmable Interval Time) Channel 0 default configuration which is updated every ~55ms, the complex alternative method would rely on modifying the IRQ0 update rate to 1ms so that we get a more precise time screening.
If you're coding a game-loop or demo-loop, you probably need a correct timestep so that your animations are consistent whatever the framerate (because using RDTSC for this purpose comes with a lot of CPU cycles penalty...).
Using such higher precision time counter can also make life easier to benchmark code, but this is another story.
Hereafter the numbers I get under MS-DOS 7.1 with my machines:
Machine | Version | Computed frequency |
---|---|---|
Compaq P200 | sleep() | 208718873 Hz / 209 Mhz |
Compaq P200 | RTC Counter | 199927588 Hz / 200 Mhz |
Siemens P200 MMX | sleep() | aaaaaaaaa Hz / zzz Mhz |
Siemens P200 MMX | RTC Counter | bbbbbbbbb Hz / yyy Mhz |
Note: the code works under 86box and dosbox-x.
[1] Beyond MS-DOS and Pentium P54C: SysToolsLib
[2] OSDev.org Detecting_CPU_Speed
[3] Intel Processor Identification and the CPUID Instruction (mirror)
[4] PC BIOS Data Areas to find RTC 40:6C
daily timer counter
[5] 8254 PIT (Programmable Interval Timer)