FAQ der Newsgroup de.comp.lang.assembler (d.c.l.a.)

Worum handelt es sich bei "rdtsc"?

Der Assembler-Befehl rdtsc liest einen prozessorinternen Zähler aus, der mit jedem Takt um Eins erhöht wird. Diesen Zähler gibt es in x86-Prozessoren offiziell seit dem Pentium II, also seit 1997. Da er aber durch das Betriebssystem abgeschaltet oder eingeschränkt werden kann, ist er nicht immer verfügbar. Allerdings schaltet kein Betriebssystem diesen Zähler standardmäßig ab. Der Zähler hat eine Breite von 64 Bits und wird mit rdtsc in das Registerpaar EDX:EAX geladen. Wenn der Assembler kein rdtsc unterstützt, kann man sich mit dw 310Fh behelfen.

Zu beachten ist, dass heutige Computer und Betriebssysteme die Taktfrequenz je nach Belastung, Temperatur und Laune ständig verändern. Eine genaue Zeitmessung ist mit rdtsc also nicht möglich.

Wenn man den Zähler einmal ausliest, und später noch einmal, kann man durch Subtraktion des ersten Wertes vom zweiten ermitteln wieviele Takte bei diesem Prozessor zwischen den beiden Auslesevorgängen vergangen sind. So kann man messen, wieviele Takte eine Reihe von Anweisungen verschlungen hat.

Man kann die Taktfrequenz des Prozessors ungefähr bestimmen, wenn man zwischen den beiden Auslesevorgängen genau eine Sekunde wartet:

Nasm & GoLink
; Name:         rdtsc.nasm
; Assemblieren: nasm.exe -fwin32 rdtsc.nasm
; Linken:       GoLink.exe /entry _main /console rdtsc.obj kernel32.dll user32.dll
; Ergebnis:     rdtsc.exe

BITS 32
EXTERN ExitProcess,Sleep,GetStdHandle,WriteFile ; kernel32.dll
EXTERN wsprintfA                                ; user32.dll

SEGMENT .bss
    Msg     RESB 1040           ; max. Output von wsprintfA
    len     RESD 1              ; Länge von Msg (Resultat wsprintfA)
    dummy   RESD 1

SEGMENT .data
    Formatstring:   db "CPU Frequenz: %lu MHz",10,0

SEGMENT .text
CpuSpeed:
    enter 8, 0                  ; 2 lokale Dword-Variablen

    rdtsc                       ; EDX:EAX = aktueller Zählerstand
    mov [ebp-8], eax            ; Resultat sichern
    mov [ebp-4], edx
    push 1000                   ; 1000 msek = 1 sek
    call Sleep                  ; kernel32.dll
    rdtsc                       ; EDX:EAX = aktueller Zählerstand
    sub eax, [ebp-8]            ; Letzten Zählerstand abziehen
    sbb edx, [ebp-4]
    mov ecx, 1000000            ; Hertz zu Megahertz
    div ecx                     ; EAX = EDX:EAX / ECX (Rest nach EDX)

    ; Ausgabe vorbereiten: len = wsprintf (Msg,"CPU Frequenz: %lu MHz\n",eax)
    push eax                    ; Argument
    push Formatstring           ; Formatanweisungen
    push Msg                    ; Ausgabepuffer
    call wsprintfA              ; user32.dll
    add esp, (3*4)              ; cdecl: 3 Dwords abstapeln
    mov [len], eax

    ; Ausgabe auf StdOut
    push -11                    ; STD_OUTPUT_HANDLE
    call GetStdHandle           ; kernel32.dll
    push 0                      ; LPOVERLAPPED lpOverlapped
    push dummy                  ; LPDWORD lpNumberOfBytesWritten
    push dword [len]            ; DWORD nNumberOfBytesToWrite
    push Msg                    ; LPCVOID lpBuffer
    push eax                    ; HANDLE hFile: StdOut von GetStdHandle
    call WriteFile              ; kernel32.dll

    leave
    ret

GLOBAL _main
_main:
    call CpuSpeed
    push 0                      ; Exitcode
    call ExitProcess            ; Ende

Ein Nasm-Programm, das mit GoLink ohne die Option /console gelinkt wird, führt bei einigen Antivirus-Programmen zu einem Trojaner-Alarm - natürlich hoffnungslos falsch. Nachfolgend der obige Algorithmus für GoAsm, wenn eine Konsole nicht benutzt werden soll (Subsystem Windows):

GoAsm & GoLink
; Name:         rdtsc.goasm
; Assemblieren: GoAsm.exe rdtsc.goasm
; Linken:       GoLink.exe /entry _main rdtsc.obj kernel32.dll user32.dll
; Ergebnis:     rdtsc.exe

.DATA
    MsgBoxText:    db 1028 DUP ?    ; max. Output von wsprintfA
    Formatstring:  db "CPU Frequenz: %lu MHz",0
    MsgBoxCaption: db "CPU Geschwindigkeit",0

.CODE
CpuSpeed FRAME
LOCALS hi, lo
    rdtsc                   ; EDX:EAX = aktueller Zählerstand
    mov [lo], eax           ; Resultat sichern
    mov [hi], edx
    INVOKE Sleep, 1000      ; kernel32.dll - 1000 ms
    rdtsc                   ; EDX:EAX = aktueller Zählerstand
    sub eax, [lo]           ; Letzten Zählerstand abziehen
    sbb edx, [hi]
    mov ecx, 1000000        ; Hertz zu Megahertz
    div ecx                 ; EAX = EDX:EAX / ECX (Rest nach EDX)
    INVOKE wsprintfA,ADDR MsgBoxText,ADDR Formatstring,eax      ; user32.dll
    add esp, (3*4)          ; cdecl: 3 Dwords abstapeln
    INVOKE MessageBoxA,0,ADDR MsgBoxText,ADDR MsgBoxCaption,0   ; user32.dll
    ret
ENDF    ; CpuSpeed

_main:
    call CpuSpeed
    INVOKE ExitProcess, 0   ; Ende Exitcode 0

Ralph 'rkhb' Bauer Apr 2009