Line | Count | Source |
1 | | #include <silk/util/tsc.h> |
2 | | |
3 | | #include <silk/util/assert.h> |
4 | | #include <silk/util/logger.h> |
5 | | #include <silk/util/platform.h> |
6 | | |
7 | | #include <cerrno> |
8 | | #include <cstring> |
9 | | |
10 | | #if defined(__x86_64__) |
11 | | # include <cpuid.h> |
12 | | #endif |
13 | | |
14 | | namespace silk |
15 | | { |
16 | | |
17 | | #if defined(__x86_64__) |
18 | | |
19 | | // CPUID leaf 0x80000007 EDX bit 8 advertises the Invariant TSC feature: |
20 | | // TSC runs at a constant rate across frequency transitions and remains synchronized |
21 | | // across cores. Silk reads TSC on one CPU and compares the result on another |
22 | | // (sleep deadlines, work-stealing budgets) -- if TSC is not invariant those |
23 | | // comparisons are unsound. Older CPUs without this bit are not supported. |
24 | | static bool hasInvariantTsc() noexcept |
25 | 327 | { |
26 | 327 | uint32_t eax, ebx, ecx, edx; |
27 | 327 | if (__get_cpuid(0x80000000, &eax, &ebx, &ecx, &edx) == 0 || eax < 0x80000007) |
28 | 0 | { |
29 | 0 | return false; |
30 | 0 | } |
31 | 327 | __cpuid(0x80000007, eax, ebx, ecx, edx); |
32 | 327 | return (edx & (1u << 8)) != 0; |
33 | 327 | } |
34 | | |
35 | | static uint64_t getTscFrequencyCpuid() noexcept |
36 | 327 | { |
37 | 327 | uint32_t eax, ebx, ecx, edx; |
38 | 327 | __cpuid(1, eax, ebx, ecx, edx); |
39 | | |
40 | | // Check for hypervisor presence (leaf 1 ECX bit 31). |
41 | 327 | if (ecx & (1u << 31)) |
42 | 327 | { |
43 | 327 | uint32_t hv_max; |
44 | 327 | __cpuid(0x40000000, hv_max, ebx, ecx, edx); |
45 | | |
46 | 327 | char vendor[13]; |
47 | 327 | memcpy(vendor + 0, &ebx, 4); |
48 | 327 | memcpy(vendor + 4, &ecx, 4); |
49 | 327 | memcpy(vendor + 8, &edx, 4); |
50 | 327 | vendor[12] = '\0'; |
51 | | |
52 | 327 | if (strcmp(vendor, "KVMKVMKVM") == 0) |
53 | 327 | { |
54 | | // KVM (AWS Nitro, GCP): leaf 0x40000010 EAX = TSC frequency in kHz. |
55 | 327 | if (hv_max >= 0x40000010) |
56 | 327 | { |
57 | 327 | __cpuid(0x40000010, eax, ebx, ecx, edx); |
58 | 327 | if (eax != 0) |
59 | 327 | { |
60 | 327 | return static_cast<uint64_t>(eax) * 1000; // kHz -> Hz |
61 | 327 | } |
62 | 327 | } |
63 | 327 | } |
64 | | // Hyper-V (Azure) does not expose a TSC frequency CPUID leaf; |
65 | | // fall through to the standard leaf 0x15 path below. |
66 | 327 | } |
67 | | |
68 | | // Bare metal and Hyper-V (Azure): leaf 0x15 TSC/crystal clock ratio. |
69 | | // EAX = denominator, EBX = numerator, ECX = crystal Hz (0 if not enumerated). |
70 | 0 | uint32_t max_leaf; |
71 | 0 | __get_cpuid(0, &max_leaf, &ebx, &ecx, &edx); |
72 | 0 | if (max_leaf < 0x15) |
73 | 0 | { |
74 | 0 | return 0; |
75 | 0 | } |
76 | | |
77 | 0 | uint32_t tsc_denom, tsc_numer, crystal_hz; |
78 | 0 | __cpuid_count(0x15, 0, tsc_denom, tsc_numer, crystal_hz, edx); |
79 | 0 | if (tsc_denom == 0 || tsc_numer == 0) |
80 | 0 | { |
81 | 0 | return 0; |
82 | 0 | } |
83 | | |
84 | 0 | if (crystal_hz != 0) |
85 | 0 | { |
86 | 0 | return static_cast<uint64_t>(crystal_hz) * tsc_numer / tsc_denom; // Hz |
87 | 0 | } |
88 | | |
89 | | // Crystal Hz not enumerated: leaf 0x16 EAX[15:0] gives the processor base |
90 | | // frequency in MHz, which equals the TSC frequency for an invariant TSC. |
91 | 0 | if (max_leaf >= 0x16) |
92 | 0 | { |
93 | 0 | uint32_t base_mhz; |
94 | 0 | __cpuid_count(0x16, 0, base_mhz, ebx, ecx, edx); |
95 | 0 | base_mhz &= 0xffff; |
96 | 0 | if (base_mhz != 0) |
97 | 0 | { |
98 | 0 | return static_cast<uint64_t>(base_mhz) * 1'000'000; // MHz -> Hz |
99 | 0 | } |
100 | 0 | } |
101 | | |
102 | 0 | return 0; |
103 | 0 | } |
104 | | |
105 | | static uint64_t getTscFrequencyCalibrated() noexcept |
106 | 0 | { |
107 | 0 | uint64_t startNs = getTimeNanoseconds(); |
108 | 0 | uint64_t startCycles = Tsc::getCycles(); |
109 | 0 | uint64_t deadlineNs = startNs + 50'000'000; // 50 ms |
110 | |
|
111 | 0 | struct timespec deadline; |
112 | 0 | deadline.tv_sec = deadlineNs / 1'000'000'000; |
113 | 0 | deadline.tv_nsec = deadlineNs % 1'000'000'000; |
114 | |
|
115 | 0 | while (clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &deadline, nullptr) == EINTR) |
116 | 0 | { |
117 | 0 | } |
118 | |
|
119 | 0 | uint64_t durationNs = getTimeNanoseconds() - startNs; |
120 | 0 | uint64_t durationCycles = Tsc::getCycles() - startCycles; |
121 | 0 | return durationCycles * 1'000'000'000 / durationNs; |
122 | 0 | } |
123 | | |
124 | | #endif // __x86_64__ |
125 | | |
126 | | void Tsc::initialize() noexcept |
127 | 327 | { |
128 | 327 | if (frequency) |
129 | 0 | { |
130 | | // Skip the second initialization. |
131 | 0 | return; |
132 | 0 | } |
133 | | |
134 | 327 | #if defined(__x86_64__) |
135 | | // Invariant TSC guarantees the counter ticks at a constant rate and stays |
136 | | // synchronized across cores -- silk reads TSC on one core and compares on |
137 | | // another, so this is what we want. Some virtualized environments (notably |
138 | | // KVM guests without nonstop_tsc) fail to advertise the bit via CPUID even |
139 | | // though rdtsc is usable, so warn and continue rather than aborting; |
140 | | // frequency then comes from CLOCK_MONOTONIC calibration below. |
141 | 327 | if (!hasInvariantTsc()) |
142 | 0 | { |
143 | 0 | SILK_WARN( |
144 | 0 | "CPU does not advertise Invariant TSC; proceeding anyway " |
145 | 0 | "-- cross-core timing may be unreliable (common on KVM guests)"); |
146 | 0 | } |
147 | 327 | frequency = getTscFrequencyCpuid(); |
148 | | // AMD CPUs do not populate CPUID leaves 0x15 or 0x16, and some older Intel |
149 | | // parts leave them empty too. Fall back to measuring TSC against |
150 | | // CLOCK_MONOTONIC over a fixed window -- the same approach the Linux kernel |
151 | | // takes when CPUID does not advertise a frequency. This is also the path |
152 | | // taken on hosts without an invariant TSC, where the CPUID frequency leaves |
153 | | // are typically absent as well. |
154 | 327 | if (frequency == 0) |
155 | 0 | { |
156 | 0 | frequency = getTscFrequencyCalibrated(); |
157 | 0 | } |
158 | | #elif defined(__aarch64__) |
159 | | // ARMv8 cntvct_el0 is anchored to a system counter that is invariant by |
160 | | // architecture, so no equivalent capability check is required. |
161 | | __asm__ volatile("mrs %0, cntfrq_el0" : "=r"(frequency)); |
162 | | #else |
163 | | # error Unsupported platform |
164 | | #endif |
165 | | |
166 | 327 | SILK_ASSERT(frequency != 0, "failed to determine TSC frequency"); |
167 | 327 | nsPerCycleFp = (1ULL << SHIFT) * 1'000'000'000 / frequency; |
168 | 327 | cyclesPerNsFp = frequency * (1ULL << SHIFT) / 1'000'000'000; |
169 | 327 | maxCyclesBeforeMultiplyOverflow = UINT64_MAX / nsPerCycleFp; |
170 | | maxNanosecondsBeforeMultiplyOverflow = UINT64_MAX / cyclesPerNsFp; |
171 | 327 | } |
172 | | |
173 | | } // namespace silk |