Strlen()

From Wiki.conus.info

Jump to: navigation, search

Еще немного о циклах. Часто, функция 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 решил что так будет лучше? Снова не берусь сказать. Но я не сомневаюсь, что эти оба варианта работают примерно равноценно в плане эффективности и скорости.

Personal tools
Namespaces
Variants
Actions
Navigation
Toolbox