машинный код x86-64 (системный вызов Linux): 78 байт
RDTSC спин-цикл синхронизации, sys_write
системный вызов Linux .
x86-64 не предоставляет удобный способ запроса частоты опорных часов RDTSC во время выполнения. Вы можете прочитать MSR (и сделать расчет на основе этого) , но для этого требуется режим ядра или root + открытие /dev/cpu/%d/msr
, поэтому я решил сделать частоту постоянной времени сборки. (Отрегулируйте FREQ_RDTSC
при необходимости: любая 32-битная константа не изменит размер машинного кода)
Обратите внимание, что процессоры x86 в течение нескольких лет имели фиксированную частоту RDTSC, поэтому ее можно использовать как источник времени, а не как счетчик производительности тактового цикла ядра, если вы не предпримете шаги, чтобы отключить изменения частоты. (Существуют фактические счетчики производительности для подсчета реальных циклов ЦП.) Обычно он работает с номинальной частотой наклейки, например, 4,0 ГГц для моего i7-6700k, независимо от турбо или энергосбережения. В любом случае, это время ожидания занятости не зависит от средней нагрузки (как откалиброванная петля задержки), а также не чувствительно к энергосбережению процессора.
Этот код будет работать для любого x86 с эталонной частотой ниже 2 ^ 32 Гц, то есть до ~ 4,29 ГГц. Кроме того, младшие 32 метки времени будут полностью перенесены за 1 секунду, поэтому мне также придется взглянуть на edx
старшие 32 бита результата.
Резюме :
нажмите 00:00:00\n
на стек. Тогда в цикле:
sys_write
системный вызов
- АЦП-цикл по цифрам (начиная с последней), чтобы увеличить время на 1. Обертывание / вынос обрабатывается с
cmp
/ cmov
, с результатом CF обеспечивает перенос следующей цифры.
rdtsc
и сохранить время начала.
- вращаться
rdtsc
до тех пор, пока дельта не станет равной тикам в секунду частоты RDTSC.
NASM листинг:
1 Address ; mov %1, %2 ; use this macro to copy 64-bit registers in 2 bytes (no REX prefix)
2 Machine code %macro MOVE 2
3 bytes push %2
4 pop %1
5 %endmacro
6
7 ; frequency as a build-time constant because there's no easy way detect it without root + system calls, or kernel mode.
8 FREQ_RDTSC equ 4000000000
9 global _start
10 _start:
11 00000000 6A0A push 0xa ; newline
12 00000002 48BB30303A30303A3030 mov rbx, "00:00:00"
13 0000000C 53 push rbx
14 ; rsp points to `00:00:00\n`
20
21 ; rbp = 0 (Linux process startup. push imm8 / pop is as short as LEA for small constants)
22 ; low byte of rbx = '0'
23 .print:
24 ; edx potentially holds garbage (from rdtsc)
25
26 0000000D 8D4501 lea eax, [rbp+1] ; __NR_write = 1
27 00000010 89C7 mov edi, eax ; fd = 1 = stdout
28 MOVE rsi, rsp
28 00000012 54 <1> push %2
28 00000013 5E <1> pop %1
29 00000014 8D5008 lea edx, [rax-1 + 9] ; len = 9 bytes.
30 00000017 0F05 syscall ; sys_write(1, buf, 9)
31
32 ;; increment counter string: least-significant digits are at high addresses (in printing order)
33 00000019 FD std ; so loop backwards from the end, wrapping each digit manually
34 0000001A 488D7E07 lea rdi, [rsi+7]
35 MOVE rsi, rdi
35 0000001E 57 <1> push %2
35 0000001F 5E <1> pop %1
36
37 ;; edx=9 from the system call
38 00000020 83C2FA add edx, -9 + 3 ; edx=3 and set CF (so the low digit of seconds will be incremented by the carry-in)
39 ;stc
40 .string_increment_60: ; do {
41 00000023 66B93902 mov cx, 0x0200 + '9' ; saves 1 byte vs. ecx.
42 ; cl = '9' = wrap limit for manual carry of low digit. ch = 2 = digit counter
43 .digitpair:
44 00000027 AC lodsb
45 00000028 1400 adc al, 0 ; carry-in = cmp from previous iteration; other instructions preserve CF
46 0000002A 38C1 cmp cl, al ; manual carry-out + wrapping at '9' or '5'
47 0000002C 0F42C3 cmovc eax, ebx ; bl = '0'. 1B shorter than JNC over a MOV al, '0'
48 0000002F AA stosb
49
50 00000030 8D49FC lea ecx, [rcx-4] ; '9' -> '5' for the tens digit, so we wrap at 59
51 00000033 FECD dec ch
52 00000035 75F0 jnz .digitpair
53 ; hours wrap from 59 to 00, so the max count is 59:59:59
54
55 00000037 AC lodsb ; skip the ":" separator
56 00000038 AA stosb ; and increment rdi by storing the byte back again. scasb would clobber CF
57
58 00000039 FFCA dec edx
59 0000003B 75E6 jnz .string_increment_60
60
61 ; busy-wait for 1 second. Note that time spent printing isn't counted, so error accumulates with a bias in one direction
62 0000003D 0F31 rdtsc ; looking only at the 32-bit low halves works as long as RDTSC freq < 2^32 = ~4.29GHz
63 0000003F 89C1 mov ecx, eax ; ecx = start
64 .spinwait:
65 ; pause
66 00000041 0F31 rdtsc ; edx:eax = reference cycles since boot
67 00000043 29C8 sub eax, ecx ; delta = now - start. This may wrap, but now we have the delta ready for a normal compare
68 00000045 3D00286BEE cmp eax, FREQ_RDTSC ; } while(delta < counts_per_second)
69 ; cmp eax, 40 ; fast count to test printing
70 0000004A 72F5 jb .spinwait
71
72 0000004C EBBF jmp .print
next address = 0x4E = size = 78 bytes.
Раскомментируйте pause
инструкцию, чтобы сэкономить значительную мощность: это нагревает одно ядро на ~ 15 градусов C без pause
, но только на ~ 9 с pause
. (На Skylake, где pause
спит ~ 100 циклов вместо ~ 5. Я думаю, что это сэкономило бы больше, если бы rdtsc
не медленная скорость, поэтому процессор не занимал много времени).
32-разрядная версия была бы на несколько байтов короче, например, при использовании 32-разрядной версии для вставки начальной строки 00: 00: 00 \ n.
16 ; mov ebx, "00:0"
17 ; push rbx
18 ; bswap ebx
19 ; mov dword [rsp+4], ebx ; in 32-bit mode, mov-imm / push / bswap / push would be 9 bytes vs. 11
А также используя 1 байт dec edx
. int 0x80
Системный вызов ABI не будет использовать еси / ЭОД, поэтому установка регистра для системных вызовов против LODSB / STOSB может быть проще.