Strlen()
From Wiki.conus.info
Еще немного о циклах. Часто, функция strlen() делается при помощи while().
Например, как это в стандартных библиотеках MSVC:
int strlen (const char * str) { const char *eos = str; while( *eos++ ) ; return( eos - str - 1 ); }
Итак, компилируем:
_eos$ = -4 ; size = 4 _str$ = 8 ; size = 4 _strlen PROC ; Line 2 push ebp mov ebp, esp push ecx ; Line 3 mov eax, DWORD PTR _str$[ebp] ; взять указатель на символ из str mov DWORD PTR _eos$[ebp], eax ; и переложить его в локальную нашу переменную eos $LN2@strlen_: ; Line 5 mov ecx, DWORD PTR _eos$[ebp] ; ecx=eos movsx edx, BYTE PTR [ecx] ; взять байт, на который указывает ecx и положить его в edx с signed-расширением mov eax, DWORD PTR _eos$[ebp] ; eax=eos add eax, 1 ; увеличить eax на еденицу mov DWORD PTR _eos$[ebp], eax ; положить eax назад в eos test edx, edx ; edx==0? je SHORT $LN1@strlen_ ; да, то что лежит в edx это ноль, выйти из цикла jmp SHORT $LN2@strlen_ ; продолжаем цикл $LN1@strlen_: ; Line 7 mov eax, DWORD PTR _eos$[ebp] ; вот здесь мы вычисляем разницу двух указателей sub eax, DWORD PTR _str$[ebp] sub eax, 1 ; отнимаем от разницы еще еденицу и возвращаем результат ; Line 8 mov esp, ebp pop ebp ret 0 _strlen_ ENDP
Здесь две новых инструкции: MOVSX и TEST.
О первой: MOVSX предназначен для того чтобы взять байт из какого-либо места в памяти и положить его, в нашем случае, в регистр EDX. Но регистр EDX - 32-битный. MOVSX означает MOV with Sign-Extent. Оставшиеся биты с 8-го по 31-й MOVSX сделает еденицей, если исходный байт в памяти имеет знак "минус", или заполнит нулями, если знак "плюс".
И вот зачем все это.
По стандарту Си, тип char - знаковый. Если у нас есть две переменные, одна char, а другая int (int тоже знаковый), и если в первой переменной лежит -2 (что кодируется как 0xFE) и мы тупо переложим это в int, то там будет 0x000000FE, а это, с точки зрения int, даже знакового, будет 254, но никак не -2. -2 в переменной int кодируется как 0xFFFFFFFE. И для того чтобы значение 0xFE из переменной типа char переложить в знаковый int с сохранением всего, нужно узнать его знак, и затем заполнить остальные биты. Это делает MOVSX.
См.также: signed number representations.
Хотя, конкретно здесь, компилятору врядли была особая надобность хранить значение char в регистре EDX а не его восьмибитной части, скажем, DL. Но получилось как получилось.
Позже выполняется TEST EDX, EDX. О инструкции TEST читайте здесь, в Битовые поля. Но конкретно здесь, эта инструкция просто проверяет состояние регистра EDX на 0.
Попробуем GCC 4.4.1:
public strlen strlen proc near eos = dword ptr -4 arg_0 = dword ptr 8 push ebp mov ebp, esp sub esp, 10h mov eax, [ebp+arg_0] mov [ebp+eos], eax loc_80483F0: mov eax, [ebp+eos] movzx eax, byte ptr [eax] test al, al setnz al add [ebp+eos], 1 test al, al jnz short loc_80483F0 mov edx, [ebp+eos] mov eax, [ebp+arg_0] mov ecx, edx sub ecx, eax mov eax, ecx sub eax, 1 leave retn strlen endp
Результат очень похож на MSVC, вот только здесь используется MOVZX а не MOVSX. MOVZX означает MOV with Zero-Extent. Эта инструкция перекладывает какое-либо значение в регистр и остальное добивает нулями. Фактически, преимущество этой инструкции только в том, что она позволяет заменить две инструкции сразу: xor eax, eax / mov al, [...].
С другой стороны, нам очевидно, что здесь можно было бы написать вот так: mov al, byte ptr [eax] / test al, al - это тоже самое, хотя старшие биты EAX будут "замусорены". Но, будем считать, что это погрешность компилятора - он не смог сделать код более экономным или более понятным. Строго говоря, компилятор вообще не обязан генерить понятный код.
Следующая новая инструкция для нас - SETNZ. В данном случае, если в AL был не ноль, то test al, al выставит флаг ZF в 0, а SETNZ, если ZF==0 (NZ значит not zero) выставит еденицу в AL. Смысл этой процедуры в том, что, если говорить человеческим языком, "если AL не ноль, то выполнить переход на loc_80483F0". Компилятор выдал немного избыточный код, но не будем забывать что оптимизация выключена.
Теперь скомпилируем все то же самое в MSVC 2010, но с включенной оптимизацией (/Ox):
_str$ = 8 ; size = 4 _strlen PROC ; Line 3 mov ecx, DWORD PTR _str$[esp-4] ; ecx -> указатель на строку mov eax, ecx ; переложить в eax $LL2@strlen_: ; Line 5 mov dl, BYTE PTR [eax] ; dl = *eax inc eax ; eax++ test dl, dl ; dl==0? jne SHORT $LL2@strlen_ ; нет, продолжаем цикл ; Line 7 sub eax, ecx ; вычисляем разницу указателей dec eax ; eax-- ; Line 8 ret 0 _strlen_ ENDP
Здесь все попроще стало. Но следует отметить, что компилятор обычно может так хорошо использовать регистры только на не очень больших функций с не очень большим количеством локальных переменных.
INC/DEC - это инструкции инкремента-декремента, попросту говоря: увеличить на еденицу или уменьшить.
Попробуем GCC 4.4.1 с влюченной оптимизацией (ключ -O3):
public strlen strlen proc near arg_0 = dword ptr 8 push ebp mov ebp, esp mov ecx, [ebp+arg_0] mov eax, ecx loc_8048418: movzx edx, byte ptr [eax] add eax, 1 test dl, dl jnz short loc_8048418 not ecx add eax, ecx pop ebp retn strlen endp
Здесь GCC не очень отстает от MSVC.
Впрочем, только кроме того что почему-то используется MOVZX, который явно можно заменить на mov dl, byte ptr [eax].
Но, возможно, компилятору GCC просто проще помнить что у него под переменную типа char отведен целый 32-битный регистр и быть уверенным в том что старшие биты регистра не будут замусорены.
Далее мы видим новую для нас инструкцию NOT. Эта инструкция инвертирует все биты в операнде. Можно сказать что здесь это синонимично инструкции XOR ECX, 0ffffffffh. NOT и следующая за ней инструкция ADD вычисляют разницу указателей и отнимают от результата еденицу. Только происходит это слегка по-другому. Сначала ECX, где хранится указатель на str, инвертируется и от него отнимается еденица.
См. также: Signed number representations
Иными словами, в конце функции, после цикла, происходит примерно следующее:
ecx=str; eax=eos; ecx=(-ecx)-1; eax=eax+ecx return eax
Что является синонимом:
ecx=str; eax=eos; eax=eax-ecx; eax=eax-1; return eax
Но почему GCC решил что так будет лучше? Снова не берусь сказать. Но я не сомневаюсь, что эти оба варианта работают примерно равноценно в плане эффективности и скорости.