[Win32] [x86 Assembler] Time Stamp Counter
久々にアセンブラ関連の記事を書きます。需要はあるのだろうか・・・
プログラムの中で、実行時間を計測したくなる場面は多々あります。そんなときはどんな API を使うでしょうか。パッと思いつくのは timeGetTime とか GetTickCount ですかね。GetSystemTime を使う人はあまりいないでしょう。QueryPerformanceCounter というのもあります。
timeGetTime 関数
http://msdn.microsoft.com/ja-jp/library/cc428795.aspx
GetTickCount 関数
http://msdn.microsoft.com/ja-jp/library/cc429827.aspx
GetSystemTime function
http://msdn.microsoft.com/en-us/library/ms724390(v=vs.85).aspx
QueryPerformanceCounter function
http://msdn.microsoft.com/en-us/library/ms644904(v=vs.85).aspx
どれを使ってもいいわけですが、とにかく厳密に測りたい場合や、一瞬で終わる処理を計測したい場合には、これらの API を使うと関数呼び出し時の時間のロスが無視できなくなります。もちろん、その分のロスを計算して結果から引くというのが普通ですが、アセンブラを使うという手も一応あります。
それが、RDTSC (=Read Time Stamp Counter) 命令です。詳細は IA-32 の仕様書に書いてありますが、RDTSC を実行すると、1 命令で Tick 値を取得することができます。しかも単位はクロック単位です。Pentium 以降の IA-32 アーキテクチャーでは、プロセッサーごとに TSC (=Time Stamp Counter) という 64bit カウンターが MSR (マシン固有レジスタ) に含まれており、RDTSC はこれを EDX:EAX レジスターにロードする命令です。
こんなプログラムを書いてみました。後でいろいろ遊ぶ伏線として、幾つかの関数を読んでいます。
なお、開発環境、実行環境は同じで、こんな感じです。
- OS: Windows 7 SP1 (x64)
- IDE: Visual Studio 2010 SP1
- CPU: Core i3 530 2.93GHz (予算がないのだ)
//
// main.cpp
//
#include <Windows.h>
#include <stdio.h>
__int64 __fastcall rdtsc() {
__asm {
rdtsc
}
}
__inline __int64 rdtsc_inline() {
__asm {
rdtsc
}
}
void rdtsc_x86(long long int *ll) {
__asm {
rdtsc
mov ecx, [ll]
mov [ecx], eax
mov [ecx+4], edx
}
}
int wmain(int argc, wchar_t *argv[]) {
if ( argc<2 )
return ERROR_INVALID_PARAMETER;
LARGE_INTEGER ll, ll1, ll2, ll3;
wprintf(L"-----\n");
DWORD wait= _wtoi(argv[1]);
while (1) {
QueryPerformanceCounter(&ll);
ll1.QuadPart= rdtsc();
ll2.QuadPart= rdtsc_inline();
rdtsc_x86((long long*)&ll3);
wprintf(L"QPC: 0x%08x`%08x\n", ll.HighPart, ll.LowPart);
wprintf(L"ASM1: 0x%08x`%08x\n", ll1.HighPart, ll1.LowPart);
wprintf(L"ASM2: 0x%08x`%08x\n", ll2.HighPart, ll2.LowPart);
wprintf(L"ASM3: 0x%08x`%08x\n", ll3.HighPart, ll3.LowPart);
wprintf(L"--\n");
Sleep(wait);
}
return 0;
}
実行すると、こんな感じです。
E:\Visual Studio 2010\Projects\asmtest\Release> asmtest 1000
-----
QPC: 0x0000000a`cdfd9d44
ASM1: 0x00002b37`f6751321
ASM2: 0x00002b37`f675135e
ASM3: 0x00002b37`f6751394
--
QPC: 0x0000000a`ce28d20e
ASM1: 0x00002b38`a3483a0d
ASM2: 0x00002b38`a3483a4a
ASM3: 0x00002b38`a3483a80
--
QPC: 0x0000000a`ce54577b
ASM1: 0x00002b39`515df112
ASM2: 0x00002b39`515df142
ASM3: 0x00002b39`515df172
--
QPC: 0x0000000a`ce802c82
ASM1: 0x00002b3a`00b20afe
ASM2: 0x00002b3a`00b20b2e
ASM3: 0x00002b3a`00b20b5e
--
ASM1 の結果に絞って間隔を計算すると、ACD326EC, AE15B705, AF5419EC となり、平均値は AE14529F = 2,920,567,455 となります。プロセッサーのクロックが 2.93GHz なので、RDTSC は確かにクロック単位の値を取得しています。
サンプルでは 3 つの関数を書きました。
- rdtsc ・・・ __fastcall 呼び出し規約
- rdtsc_inline ・・・ __inline でインライン展開
- rdtsc_x86 ・・・ パラメーター渡し
ソースコードを既定の Release ビルド設定でビルドすると、rdtsc_inline だけでなく rdtsc_x86 もインライン展開されます。rdtsc に __fastcall をつけたのはインライン展開を防ぐためで、__stdcall や __cdecl では展開されてしまいます。
コンパイルされた関数のアセンブラを見てみます。
0:000> uf asmtest!rdtsc
asmtest!rdtsc [e:\visual studio 2010\projects\asmtest\main.cpp @ 9]:
9 011e1000 0f31 rdtsc
13 011e1002 c3 ret
0:000> uf asmtest!wmain
asmtest!wmain [e:\visual studio 2010\projects\asmtest\main.cpp @ 30]:
30 011e1010 55 push ebp
30 011e1011 8bec mov ebp,esp
30 011e1013 83e4f8 and esp,0FFFFFFF8h
30 011e1016 83ec2c sub esp,2Ch
31 011e1019 837d0802 cmp dword ptr [ebp+8],2
31 011e101d 53 push ebx
31 011e101e 56 push esi
31 011e101f 57 push edi
31 011e1020 7d0c jge asmtest!wmain+0x1e (011e102e)
asmtest!wmain+0x12 [e:\visual studio 2010\projects\asmtest\main.cpp @ 55]:
55 011e1022 5f pop edi
55 011e1023 5e pop esi
55 011e1024 b857000000 mov eax,57h
55 011e1029 5b pop ebx
55 011e102a 8be5 mov esp,ebp
55 011e102c 5d pop ebp
55 011e102d c3 ret
asmtest!wmain+0x1e [e:\visual studio 2010\projects\asmtest\main.cpp @ 36]:
36 011e102e 8b359c201e01 mov esi,dword ptr [asmtest!_imp__wprintf (011e209c)]
36 011e1034 68f4201e01 push offset asmtest!`string' (011e20f4)
36 011e1039 ffd6 call esi
38 011e103b 8b450c mov eax,dword ptr [ebp+0Ch]
38 011e103e 8b4804 mov ecx,dword ptr [eax+4]
38 011e1041 51 push ecx
38 011e1042 ff15a0201e01 call dword ptr [asmtest!_imp___wtoi (011e20a0)]
38 011e1048 83c408 add esp,8
38 011e104b 89442414 mov dword ptr [esp+14h],eax
38 011e104f 90 nop
asmtest!wmain+0x40 [e:\visual studio 2010\projects\asmtest\main.cpp @ 40]:
40 011e1050 8d542418 lea edx,[esp+18h]
40 011e1054 52 push edx
40 011e1055 ff1500201e01 call dword ptr [asmtest!_imp__QueryPerformanceCounter (011e2000)]
41 011e105b e8a0ffffff call asmtest!rdtsc (011e1000)
41 011e1060 8bd8 mov ebx,eax
41 011e1062 8954242c mov dword ptr [esp+2Ch],edx
42 011e1066 0f31 rdtsc
42 011e1068 8bf8 mov edi,eax
43 011e106a 8d442420 lea eax,[esp+20h]
43 011e106e 89542434 mov dword ptr [esp+34h],edx
43 011e1072 89442410 mov dword ptr [esp+10h],eax
43 011e1076 0f31 rdtsc
43 011e1078 8b4c2410 mov ecx,dword ptr [esp+10h]
43 011e107c 8901 mov dword ptr [ecx],eax
43 011e107e 895104 mov dword ptr [ecx+4],edx
45 011e1081 8b4c2418 mov ecx,dword ptr [esp+18h]
45 011e1085 8b54241c mov edx,dword ptr [esp+1Ch]
45 011e1089 51 push ecx
45 011e108a 52 push edx
45 011e108b 6804211e01 push offset asmtest!`string' (011e2104)
45 011e1090 ffd6 call esi
46 011e1092 8b442438 mov eax,dword ptr [esp+38h]
46 011e1096 53 push ebx
46 011e1097 50 push eax
46 011e1098 682c211e01 push offset asmtest!`string' (011e212c)
46 011e109d ffd6 call esi
47 011e109f 8b4c244c mov ecx,dword ptr [esp+4Ch]
47 011e10a3 57 push edi
47 011e10a4 51 push ecx
47 011e10a5 6854211e01 push offset asmtest!`string' (011e2154)
47 011e10aa ffd6 call esi
48 011e10ac 8b542444 mov edx,dword ptr [esp+44h]
48 011e10b0 8b442448 mov eax,dword ptr [esp+48h]
48 011e10b4 52 push edx
48 011e10b5 50 push eax
48 011e10b6 687c211e01 push offset asmtest!`string' (011e217c)
48 011e10bb ffd6 call esi
50 011e10bd 68a4211e01 push offset asmtest!`string' (011e21a4)
50 011e10c2 ffd6 call esi
51 011e10c4 8b4c2448 mov ecx,dword ptr [esp+48h]
51 011e10c8 83c434 add esp,34h
51 011e10cb 51 push ecx
51 011e10cc ff1504201e01 call dword ptr [asmtest!_imp__Sleep (011e2004)]
52 011e10d2 e979ffffff jmp asmtest!wmain+0x40 (011e1050)
関数 rdtsc は、RDTSC を実行するだけの関数になっています。インライン展開された関数は茶色と紫色で示しています。戻り値が EDX:EAX であることが考慮されて、うまく動くようになっています。edx レジスタが考慮されないのかと予想していましたが、インライン展開されても 64bit の戻り値に影響はないようです。
さて、次に QueryPerformanceCounter に注目してみます。上述の MSDN の説明を見ると、こんな注意書きがあります。
On a multiprocessor computer, it should not matter which processor is called. However, you can get different results on different processors due to bugs in the basic input/output system (BIOS) or the hardware abstraction layer (HAL). To specify processor affinity for a thread, use the SetThreadAffinityMask function.
「マルチ プロセッサーでも使えますよ。でも BIOS や HAL にバグがあるとプロセッサー毎に異なる結果が返ってくることがありますよ。」 ということです。BIOS や HAL のバグって言われても・・・既知のバグなら直しとけよ、としか言いようがありません。QueryPerformanceCounter を使うときは SetThreadAffinityMask を必ず使えってことなのでしょうか。微妙です。
少なくとも、QueryPerformanceCounter にはマルチプロセッサーを考慮した実装がなされていることが分かります。TSC カウンターはプロセッサー毎なので、先ほどの RDTSC 命令を呼び出す関数は、プロセッサー毎に異なる値を返します。よって、途中で割り込みが入って実行 CPU が変わることが予想される場合には使えません。
デバッグしていて気づきましたが、実は QueryPerformanceCounter は内部的に RDTSC を呼び出しています。これを確かめてみます。
0:000> x kernel32!QueryPerformanceCounter
76921732 kernel32!QueryPerformanceCounter = <no type information>
0:000> u 76921732
kernel32!QueryPerformanceCounter:
76921732 ff25d40d9276 jmp dword ptr [kernel32!_imp__QueryPerformanceCounter (76920dd4)]
76921738 90 nop
76921739 90 nop
7692173a 90 nop
7692173b 90 nop
7692173c 90 nop
kernel32!IsDBCSLeadByte:
7692173d ff2568079276 jmp dword ptr [kernel32!_imp__IsDBCSLeadByte (76920768)]
76921743 90 nop
0:000> dd 76920dd4 l1
76920dd4 775e8884
0:000> ln 775e8884
(775e8884) ntdll!RtlQueryPerformanceCounter | (775e88e2) ntdll!EtwEventEnabled
Exact matches:
ntdll!RtlQueryPerformanceCounter = <no type information>
QueryPerformanceCounter は Kernel32.dll の関数ですが、これは単なるラッパーで、実体は ntdll.dll に実装されている RtlQueryPerformanceCounter であることが分かります。この関数のアセンブラを見てみます。
0:000> uf ntdll!RtlQueryPerformanceCounter
ntdll!RtlQueryPerformanceCounter:
775e8884 8bff mov edi,edi
775e8886 55 push ebp
775e8887 8bec mov ebp,esp
775e8889 51 push ecx
775e888a 51 push ecx
775e888b f605ed02fe7f01 test byte ptr [SharedUserData+0x2ed (7ffe02ed)],1
775e8892 0f840bf50400 je ntdll!RtlQueryPerformanceCounter+0x55 (77637da3)
ntdll!RtlQueryPerformanceCounter+0x10:
775e8898 56 push esi
ntdll!RtlQueryPerformanceCounter+0x11:
775e8899 8b0db803fe7f mov ecx,dword ptr [SharedUserData+0x3b8 (7ffe03b8)]
775e889f 8b35bc03fe7f mov esi,dword ptr [SharedUserData+0x3bc (7ffe03bc)]
775e88a5 a1b803fe7f mov eax,dword ptr [SharedUserData+0x3b8 (7ffe03b8)]
775e88aa 8b15bc03fe7f mov edx,dword ptr [SharedUserData+0x3bc (7ffe03bc)]
775e88b0 3bc8 cmp ecx,eax
775e88b2 75e5 jne ntdll!RtlQueryPerformanceCounter+0x11 (775e8899)
ntdll!RtlQueryPerformanceCounter+0x2c:
775e88b4 3bf2 cmp esi,edx
775e88b6 75e1 jne ntdll!RtlQueryPerformanceCounter+0x11 (775e8899)
ntdll!RtlQueryPerformanceCounter+0x30:
775e88b8 0f31 rdtsc
775e88ba 03c1 add eax,ecx
775e88bc 0fb60ded02fe7f movzx ecx,byte ptr [SharedUserData+0x2ed (7ffe02ed)]
775e88c3 13d6 adc edx,esi
775e88c5 c1e902 shr ecx,2
775e88c8 e893ffffff call ntdll!_aullshr (775e8860)
775e88cd 8b4d08 mov ecx,dword ptr [ebp+8]
775e88d0 8901 mov dword ptr [ecx],eax
775e88d2 895104 mov dword ptr [ecx+4],edx
775e88d5 5e pop esi
ntdll!RtlQueryPerformanceCounter+0x4e:
775e88d6 33c0 xor eax,eax
775e88d8 40 inc eax
ntdll!RtlQueryPerformanceCounter+0x51:
775e88d9 c9 leave
775e88da c20400 ret 4
ntdll!RtlQueryPerformanceCounter+0x55:
77637da3 8d45f8 lea eax,[ebp-8]
77637da6 50 push eax
77637da7 ff7508 push dword ptr [ebp+8]
77637daa e8717ff9ff call ntdll!ZwQueryPerformanceCounter (775cfd20)
77637daf 85c0 test eax,eax
77637db1 7d0d jge ntdll!RtlQueryPerformanceCounter+0x6f (77637dc0)
ntdll!RtlQueryPerformanceCounter+0x65:
77637db3 50 push eax
77637db4 e89549fdff call ntdll!RtlSetLastWin32ErrorAndNtStatusFromNtStatus (7760c74e)
ntdll!RtlQueryPerformanceCounter+0x6b:
77637db9 33c0 xor eax,eax
77637dbb e9190bfbff jmp ntdll!RtlQueryPerformanceCounter+0x51 (775e88d9)
ntdll!RtlQueryPerformanceCounter+0x6f:
77637dc0 837df800 cmp dword ptr [ebp-8],0
77637dc4 0f850c0bfbff jne ntdll!RtlQueryPerformanceCounter+0x4e (775e88d6)
ntdll!RtlQueryPerformanceCounter+0x75:
77637dca 837dfc00 cmp dword ptr [ebp-4],0
77637dce 0f85020bfbff jne ntdll!RtlQueryPerformanceCounter+0x4e (775e88d6)
ntdll!RtlQueryPerformanceCounter+0x7b:
77637dd4 6a78 push 78h
77637dd6 e814a5f9ff call ntdll!RtlSetLastWin32Error (775d22ef)
77637ddb ebdc jmp ntdll!RtlQueryPerformanceCounter+0x6b (77637db9)
RDTSC 命令がありました。さっきのサンプルプログラムでこの部分にブレークポイントを貼ると、確かに RDTSC が実行されていることが分かります。
0:002> bp 775e88b8
0:002> g
Breakpoint 0 hit
eax=00000000 ebx=14c34d78 ecx=00000000 edx=00000000 esi=00000000 edi=14c34da8
eip=775e88b8 esp=0045fe80 ebp=0045fe8c iopl=0 nv up ei pl zr na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246
ntdll!RtlQueryPerformanceCounter+0x30:
775e88b8 0f31 rdtsc
0:000> k
ChildEBP RetAddr
0045fe8c 00bb105b ntdll!RtlQueryPerformanceCounter+0x30
0045fecc 6a19263d asmtest!wmain+0x4b
0045ff18 7692339a MSVCR100!_initterm+0x13
0045ff24 775e9ed2 kernel32!BaseThreadInitThunk+0xe
0045ff64 775e9ea5 ntdll!__RtlUserThreadStart+0x70
0045ff7c 00000000 ntdll!_RtlUserThreadStart+0x1b
プログラムの実行結果を見ると分かりますが、QueryPerformanceCounter (以下 QPC と表記) と RDTSC の戻り値は全然違います。単位も異なっているようです。QPC の周波数は QueryPerformanceFrequency API で取得することができます。この環境で調べてみると、0x2BD8E6 = 2,852,116 となりました。クロックの 1/1000 ぐらいです。
QPC が RDTSC の値を元にしていることは確認できているので、何らかの計算をして周波数を調整していることになります。それが RtlQueryPerformanceCounter のアセンブラに隠されています。RDTSC の後のアセンブラをもう一度よく見てみます。
ntdll!RtlQueryPerformanceCounter+0x30:
775e88b8 0f31 rdtsc
775e88ba 03c1 add eax,ecx
775e88bc 0fb60ded02fe7f movzx ecx,byte ptr [SharedUserData+0x2ed (7ffe02ed)]
775e88c3 13d6 adc edx,esi
775e88c5 c1e902 shr ecx,2
775e88c8 e893ffffff call ntdll!_aullshr (775e8860)
775e88cd 8b4d08 mov ecx,dword ptr [ebp+8]
775e88d0 8901 mov dword ptr [ecx],eax
775e88d2 895104 mov dword ptr [ecx+4],edx
775e88d5 5e pop esi
ntdll!RtlQueryPerformanceCounter+0x4e:
775e88d6 33c0 xor eax,eax
775e88d8 40 inc eax
ntdll!RtlQueryPerformanceCounter+0x51:
775e88d9 c9 leave
775e88da c20400 ret 4
_anullshr という関数が怪しいですね。これを見てみます。
0:000> uf ntdll!_aullshr
ntdll!_aullshr:
775e8860 80f940 cmp cl,40h
775e8863 7315 jae ntdll!_aullshr+0x1a (775e887a)
ntdll!_aullshr+0x5:
775e8865 80f920 cmp cl,20h
775e8868 7306 jae ntdll!_aullshr+0x10 (775e8870)
ntdll!_aullshr+0xa:
775e886a 0fadd0 shrd eax,edx,cl
775e886d d3ea shr edx,cl
775e886f c3 ret
ntdll!_aullshr+0x10:
775e8870 8bc2 mov eax,edx
775e8872 33d2 xor edx,edx
775e8874 80e11f and cl,1Fh
775e8877 d3e8 shr eax,cl
775e8879 c3 ret
ntdll!_aullshr+0x1a:
775e887a 33c0 xor eax,eax
775e887c 33d2 xor edx,edx
775e887e c3 ret
eax と edx をcl だけ右シフトしています。これで QPC の結果が小さくなるわけです。
では ECX レジスタはどこから来ていたでしょうか。
775e88bc 0fb60ded02fe7f movzx ecx,byte ptr [SharedUserData+0x2ed (7ffe02ed)]
775e88c3 13d6 adc edx,esi
775e88c5 c1e902 shr ecx,2
775e88c8 e893ffffff call ntdll!_aullshr (775e8860)
SharedUserData から来ています。これは共有ユーザーモードページと呼ばれるデータで、デバッガー コマンドの !kuser で概要を表示できます。詳しくはデバッガーのヘルプを参照して下さい。
0:000> !kuser
_KUSER_SHARED_DATA at 7ffe0000
TickCount: fa00000 * 000000000011d55a (0:05:04:21.406)
TimeZone Id: 0
ImageNumber Range: [8664 .. 8664]
Crypto Exponent: 0
SystemRoot: 'C:\Windows'
ここでちょっとしたトリックが必要になります。
実はユーザーモード側から _KUSER_SHARED_DATA 構造体を見ても、全メンバーを見ることができません。そこで、カーネル モードから見る必要があります。同じ環境で livekd を使った出力がこれです。
Microsoft Windows [Version 6.1.7601]
Copyright (c) 2009 Microsoft Corporation. All rights reserved.
E:\Visual Studio 2010\Projects\asmtest\Release>livekd
LiveKd v5.0 - Execute kd/windbg on a live system
Sysinternals - www.sysinternals.com
Copyright (C) 2000-2010 Mark Russinovich and Ken Johnson
Launching c:\debuggers\amd64\kd.exe:
Microsoft (R) Windows Debugger Version 6.12.0002.633 AMD64
Copyright (c) Microsoft Corporation. All rights reserved.
Loading Dump File [C:\Windows\livekd.dmp]
Kernel Complete Dump File: Full address space is available
Comment: 'LiveKD live system view'
Symbol search path is: srv*c:\websymbols*http://msdl.microsoft.com/download/symbols
Executable search path is:
Windows 7 Kernel Version 7601 (Service Pack 1) MP (4 procs) Free x64
Product: WinNt, suite: TerminalServer SingleUserTS
Built by: 7601.17640.amd64fre.win7sp1_gdr.110622-1506
Machine Name:
Kernel base = 0xfffff800`03a63000 PsLoadedModuleList = 0xfffff800`03ca8670
Debug session time: Sun Feb 13 11:34:57.897 17420 (UTC + 9:00)
System Uptime: 0 days 5:44:06.294
Loading Kernel Symbols
...............................................................
................................................................
..........................................
Loading User Symbols
............
Loading unloaded module list
........Unable to enumerate user-mode unloaded modules, NTSTATUS 0xC0000147
0: kd> !kuser
*** ERROR: Module load completed but symbols could not be loaded for LiveKdD.SYS
_KUSER_SHARED_DATA at fffff78000000000
TickCount: fa00000 * 0000000000142992 (0:05:44:06.281)
TimeZone Id: 0
ImageNumber Range: [8664 .. 8664]
Crypto Exponent: 0
SystemRoot: 'C:\Windows'
0: kd> dt _KUSER_SHARED_DATA fffff78000000000
ntdll!_KUSER_SHARED_DATA
+0x000 TickCountLowDeprecated : 0
(省略)
+0x2ed TscQpcData : 0x29 ')'
+0x2ed TscQpcEnabled : 0y1
+0x2ed TscQpcSpareFlag : 0y0
+0x2ed TscQpcShift : 0y001010 (0xa)
+0x2ee TscQpcPad : [2] ""
(省略)
ntdll!_aullshr に渡されていたデータは SharedUserData+0x2ed でした。+2ed のところには、TscQpc*** という、まさに TSC と QPC に関連した値が保存されていることが分かります。
_KUSER_SHARED_DATA は公開されている構造体なので、Windows Driver Kit に含まれる ntddk.h で定義を確認することができます。それがこれです。
//
// The following byte is consumed by the user-mode performance counter
// routines to improve latency when the source is the processor's cycle
// counter.
//
union {
UCHAR TscQpcData;
struct {
UCHAR TscQpcEnabled : 1;
UCHAR TscQpcSpareFlag : 1;
UCHAR TscQpcShift : 6;
} DUMMYSTRUCTNAME;
} DUMMYUNIONNAME;
ビットフィールドになっていて、3 つのフィールドがあります。名前から推測できてしまいますが、一応アセンブラで確認していきましょう。ntdll!RtlQueryPerformanceCounter において、ntdll!_aullshr を呼び出す前に shr ecx,2 という命令で 2 ビット右シフトさせます。これは何かというと、TscQpcData を 2 ビット右シフト、つまり、ビットフィールドの TscQpcShift を取得していることになります。で、カーネル デバッガーの出力結果を見るとこの値は TscQpcShift : 0y001010 (0xa) 、つまり 10 です。これで謎が解けました。
RDTSC の結果と QPC の結果が 1000 倍ぐらい違っていましたが、これは TscQpcShift の値だけ右シフトした値、つまり 1024 倍異なっていたことになります。これで QPC の仕組みは大体分かりました。
もう一度 ntdll!RtlQueryPerformanceCounter のアセンブラに戻ります。ntdll!RtlQueryPerformanceCounter+0x51 の後に ret 命令があって、ここで RDTSC を右シフトした値を返して終わるわけですが、関数自体は続いています。これはいつ呼ばれるでしょうか。それが関数のアタマにある、この部分です。
775e888b f605ed02fe7f01 test byte ptr [SharedUserData+0x2ed (7ffe02ed)],1
775e8892 0f840bf50400 je ntdll!RtlQueryPerformanceCounter+0x55 (77637da3)
また SharedUserData の値を使っています。+2ed なので TscQpcData の値ですが、今度は最下位ビットを test 命令で調べています。つまり TscQpcEnabled です。これが 0 の場合は、RDTSC が使われずに、ntdll!ZwQueryPerformanceCounter が呼ばれることになります。つまりカーネル モードの関数が呼ばれます。この関数が何を使っているのかについては、ここでは触れません。