2011-02-25 09:08:03 UTC
При написании crc32 калькулятора, а точнее при попытке оптимизации алгоритма расчета crc32, я столкнулся с интересным выводом, — вроде бы с виду более быстрый код, оказывается более медленным. Ещё один факт в копилку того, что нельзя делать выводы о производительности, основываясь на интуиции и не делая замеров — в 9 случаях из 10 вы окажетесь неправы.
Итак, имеем две реализации функции расчета crc32, первая:
void Crc32Update(Crc32Context* ctx, const void* data, uint32_t len)
{
uint32_t i = 0;
const uint8_t* block = data;
while (i < len) {
ctx->crc = ((ctx->crc >> 8) & 0x00FFFFFF) ^ crcTab[(ctx->crc ^ *block++) & 0xFF];
++i;
}
}
И вторая.
void Crc32Update(Crc32Context* ctx, const void* data, long len)
{
const uint8_t* block = data;
while (--len >= 0) {
ctx->crc = ((ctx->crc >> 8) & 0x00FFFFFF) ^ crcTab[(ctx->crc ^ *block++) & 0xFF];
}
}
Вторая с виду кажется быстрее, — компактнее, нет лишней локальной переменной, но это только с виду. На самом деле, она медленнее процентов на 10 первой функции.
Посмотрим, что же сгенерировал компилятор для обеих функций, первой:
PUBLIC _Crc32Update
; Function compile flags: /Ogtp
; COMDAT _Crc32Update
_TEXT SEGMENT
_Crc32Update PROC ; COMDAT
; _ctx$ = edx
; _data$ = edi
; _len$ = esi
; 70 : uint32_t i = 0;
xor ecx, ecx
; 71 : const uint8_t* block = data;
; 72 :
; 73 : while (i < len) {
test esi, esi
je SHORT $LN1@Crc32Updat
push ebx
$LL2@Crc32Updat:
; 74 : ctx->crc = ((ctx->crc >> 8) & 0x00FFFFFF) ^ crcTab[(ctx->crc ^ *block++) & 0xFF];
movzx ebx, BYTE PTR [ecx+edi]
mov eax, DWORD PTR [edx]
xor ebx, eax
and ebx, 255 ; 000000ffH
shr eax, 8
xor eax, DWORD PTR _crcTab[ebx*4]
; 75 : ++i;
inc ecx
mov DWORD PTR [edx], eax
cmp ecx, esi
jb SHORT $LL2@Crc32Updat
pop ebx
$LN1@Crc32Updat:
; 76 : }
; 77 : }
ret 0
_Crc32Update ENDP
}
И второй:
PUBLIC _Crc32Update
; Function compile flags: /Ogtp
; COMDAT _Crc32Update
_TEXT SEGMENT
_len$ = 8 ; size = 4
_Crc32Update PROC ; COMDAT
; _ctx$ = edx
; _data$ = ecx
; 69 : {
push ebp
mov ebp, esp
push esi
mov esi, DWORD PTR _len$[ebp]
; 70 : const uint8_t* block = data;
; 71 :
; 72 : while (--len >= 0) {
dec esi
js SHORT $LN1@Crc32Updat
push edi
npad 5
$LL2@Crc32Updat:
; 73 : ctx->crc = ((ctx->crc >> 8) & 0x00FFFFFF) ^ crcTab[(ctx->crc ^ *block++) & 0xFF];
movzx edi, BYTE PTR [ecx]
mov eax, DWORD PTR [edx]
xor edi, eax
and edi, 255 ; 000000ffH
shr eax, 8
xor eax, DWORD PTR _crcTab[edi*4]
inc ecx
dec esi
mov DWORD PTR [edx], eax
jns SHORT $LL2@Crc32Updat
pop edi
$LN1@Crc32Updat:
pop esi
; 74 : }
; 75 : }
pop ebp
ret 0
_Crc32Update ENDP
}
Что же видно в результате сравнения:
- Так как параметр модифицируется в коде, он вынужден выгрузить её из стека в регистр — несколько лишних команд. В первом же варианте, видя, что ничего не меняется и количество аргументов функции маленькое, он смог оптимизировать и засунуть параметр в регистр esi.
- Дополнительно, перед каждым запуском цикла, есть уменьшение значения регистра, в который загружен этот параметр, чего нет в другом варианте.
В итоге, если функция вызывается миллионы или миллиарды раз, эти дополнительные инструкции, начинают вносить свой вклад в замедление.