Optimización de strlen: Alineación de 4 bytes y Análisis de Desensamblado

La necesidad de manejar la alineación de 4 bytes en funciones de manipulación de cadenas en C se debe principalmente a la eficiencia. El acceso a datos no alineados puede requerir que la CPU realice operaciones adicionales de lectura y reorganización, lo que disminuye el rendimiento. Además, la alineación es crucial para la comunicación entre sistemas o procesos: si las estructuras de datos no están alineadas de manera consistente en diferentes extremos, pueden producirse errores de comunicación. Algunas arquitecturas de CPU incluso carecen de soporte para accesos no alineados, lo que puede provocar excepciones o fallos catastróficos del programa.

Principios de la función strlen Al desensamblar la biblioteca de cadenas de Visual Studio 2019, se observa que muchas de estas funciones están implementadas en ensamblador. En lugar de iterar byte a byte para encontrar el terminador nulo ('\0'), las implementaciones optimizadas, como la de strlen, suelen procesar los datos en bloques de 4 bytes. Los pasos generales son:

  1. Verificación de alineación de 4 bytes: Se comprueba si la dirección actual está alineada a 4 bytes (por ejemplo, usando la expresión addr & 0x3 == 0). Si está alineada, se procede al paso 2. Si no, se recurre a una comprobación byte a byte hasta que se alcance una dirección alineada o se encuentre el terminador nulo.
  2. Detección de terminador nulo en bloque de 4 bytes: Se utiliza un conjunto de operaciones aritméticas y lógicas para determinar si alguno de los 4 bytes dentro del bloque actual es un carácter nulo. Una técnica común implica las siguientes operaciones (donde valor es el bloque de 4 bytes): ```

a = valor + 0x7efefeff; valor = valor ^ 0xffffffff; // Equivalente a 0xffffffff - valor valor = valor ^ a; valor = valor & 0x81010100;


Si el resultado de esta operación es cero, significa que ninguno de los bytes en el bloque es un carácter nulo. Si el resultado no es cero, indica la presencia de al menos un carácter nulo.
3. **Localización precisa del terminador nulo:** Si se detecta un terminador nulo en el bloque de 4 bytes, se realizan comprobaciones bit a bit para identificar su posición exacta dentro del bloque.
4. **Cálculo de la longitud:** La longitud de la cadena se calcula restando la dirección de inicio de la cadena de la dirección del terminador nulo encontrado.

Alineación de 4 bytes
La razón principal para verificar la alineación de 4 bytes es la eficiencia de acceso a la memoria. Los procesadores de 32 bits suelen poder leer 4 bytes de memoria de manera eficiente si la dirección de inicio de esa lectura está alineada a un múltiplo de 4. Si la lectura abarca límites de 4 bytes (por ejemplo, leer desde la dirección 0x3 hasta la 0x7), la CPU podría necesitar realizar dos lecturas separadas y reorganizar los datos, lo que resulta considerablemente más lento.

Manejo de direcciones no alineadas
Cuando una dirección no está alineada a 4 bytes, existen varias estrategias:
1. **Procesamiento byte a byte:** Se itera sobre los bytes individuales hasta encontrar el terminador nulo o alcanzar una dirección alineada.
2. **Optimización en la biblioteca:** Funciones como `strcmp` (y por extensión, `strlen`) pueden incluir optimizaciones para manejar direcciones no alineadas. Por ejemplo, si la dirección no está alineada a 4 bytes (`addr & 3 != 0`), se pueden realizar ajustes incrementando la dirección hasta que sea alineada o hasta que se encuentre el terminador nulo. Esto se puede lograr examinando los bits menos significativos de la dirección para determinar cuántos bytes deben procesarse individualmente antes de pasar a operaciones de 4 bytes.

Análisis de la fórmula de detección de nulos
La fórmula utilizada para detectar un carácter nulo dentro de un bloque de 4 bytes se basa en transformaciones bitwise:
- La operación `valor ^ 0xffffffff` es matemáticamente equivalente a `0xffffffff - valor`.
Consideremos la expresión completa:

a = valor + 0x7efefeff; valor = valor ^ 0xffffffff; valor = valor ^ a; valor = valor & 0x81010100;

Esto se puede combinar en una sola expresión:

resultado = ((valor + 0x7efefeff) ^ (0xffffffff - valor)) & 0x81010100;

Esta expresión se puede desglosar byte por byte para entender cómo las operaciones de suma, XOR y AND interactúa para identificar la presencia de un cero en cualquiera de los bytes originales. La máscara `0x81010100` está diseñada para aislar los bits específicos que indican la presencia de un cero después de las transformaciones.

Implementación en C
Una posible ipmlementación en C que intenta emular esta lógica de optimización podría verse así:

int optimized_strlen(const char* str) {
const char* start = str;
const int* p = (const int*)str;
int len = 0;

// Procesar bloques de 4 bytes mientras estén alineados
while (((unsigned long)p & 3) == 0) {
int val = *p;
// Comprobar si hay un byte nulo en el bloque de 4 bytes
int check = ((val + 0x7efefeff) ^ (0xffffffff - val)) & 0x81010100;
if (check != 0) {
// Se encontró un carácter nulo, pasar a la comprobación byte a byte
str = (const char*)p;
break;
}
p++;
}

// Si salimos del bucle anterior por falta de alineación o porque 'str' ya estaba desalineado
// o si encontramos un bloque con nulo, necesitamos procesar byte a byte desde 'str'
// o desde el inicio del bloque de 4 bytes si 'p' se incrementó
if (((unsigned long)p & 3) != 0) {
str = (const char*)p; // Asegurarse de que 'str' apunte a la posición correcta
}


while (*str != '\0') {
str++;
}
len = str - start;
return len;
}

// Nota: La implementación C original proporcionada en la fuente es más compleja y
// maneja explícitamente las posiciones de los bytes nulos dentro del bloque.
// La siguiente es una versión simplificada que se enfoca en el concepto.

int myStrlen(char buf[]) {
int* p = (int*)buf; // Usar puntero a int para operaciones de 4 bytes
int len = 0;
while (1) {
// La fórmula original era:
// int tmp = ((*p + 0x8efefeff) ^ (~(*p))) & 0x81010100;
// Corrigiendo la operación ~(*p) a 0xffffffff - (*p) para mayor claridad y corrección
int val = *p;
int tmp = ((val + 0x7efefeff) ^ (0xffffffff - val)) & 0x81010100;

if (tmp == 0) { // Si tmp es 0, no hay caracteres nulos en este bloque
p++; // Avanzar al siguiente bloque de 4 bytes
continue;
} else {
// Se encontró un carácter nulo en el bloque actual.
// Calcular la longitud hasta el inicio de este bloque.
len = (char*)p - buf;

// Ahora, encontrar la posición exacta del primer carácter nulo
// en el bloque actual (desde la posición 'buf + len')
// El código original usaba máscaras de bits para determinar esto.
// Una forma más directa sería iterar byte a byte desde 'p'.
// Sin embargo, para emular la lógica de la fuente, usaremos sus comprobaciones:

// Comprobamos el byte menos significativo
if (!(*p & 0x000000ff)) { // Si el primer byte es 0
len += 0; // La longitud ya incluye el inicio del bloque
break;
}
// Comprobamos el segundo byte
else if (!(*p & 0x0000ff00)) { // Si el segundo byte es 0
len += 1;
break;
}
// Comprobamos el tercer byte
else if (!(*p & 0x00ff0000)) { // Si el tercer byte es 0
len += 2;
break;
}
// Comprobamos el cuarto byte
else if (!(*p & 0xff000000)) { // Si el cuarto byte es 0
len += 3;
break;
}
// Si ninguna de las anteriores se cumple, significa que el carácter nulo
// estaba en uno de los bytes procesados pero la lógica anterior no lo detectó
// (esto podría ocurrir en casos límite o si la lógica original tenía alguna sutileza).
// Sin embargo, basándonos en la estructura, uno de los ifs debería cumplirse si tmp != 0.
// Asumiendo que si tmp != 0, al menos uno de los bytes es nulo.
}
}
return len;
}


; mov ecx, [ebp + 8] ; Copiar la dirección de inicio a ECX ; test ecx, 3 ; Comprobar si ECX está alineado a 4 bytes (bits 0 y 1 son 0) ; je asm_strlen@main_loop ; Si está alineado, saltar al bucle principal

; asm_strlen@misligned: ; Etiqueta para el manejo de direcciones no alineadas ; mov al, byte ptr[ecx] ; Cargar el byte actual en AL ; add ecx, 1 ; Incrementar el puntero ; test al, al ; Comprobar si el byte es nulo ; je byte_0 ; Si es nulo, saltar a la lógica de cálculo final ; test ecx, 3 ; Volver a comprobar la alineación ; jne asm_strlen@misligned ; Si todavía no está alineado, continuar procesando byte a byte

; asm_strlen@main_loop: ; Bucle principal para procesar bloques de 4 bytes ; mov eax, dword ptr[ecx] ; Cargar 4 bytes (un DWORD) en EAX ; add ecx, 4 ; Avanzar el puntero en 4 bytes ; mov edx, eax ; Copiar el valor a EDX para la siguiente operación ; add edx, 0x7efefeff ; Suma para la transformación ; xor eax, 0xffffffff ; Invertir bits de EAX (0xffffffff - original_eax) ; xor eax, edx ; XOR con el valor modificado en EDX ; test eax, 0x81010100 ; Aplicar la máscara de detección ; je asm_strlen@main_loop ; Si el resultado es 0, no hay nulos, continuar el bucle

; ; Si el test falló, significa que se encontró un carácter nulo en el bloque anterior (antes de add ecx, 4) ; ; Por lo tanto, necesitamos retroceder el puntero y encontrar el nulo exacto. ; mov eax, dword ptr[ecx - 4] ; Cargar el bloque de 4 bytes que contenía el nulo

; ; Ahora, determinar la posición del nulo dentro de este bloque ; test al, al ; Comprobar el primer byte (byte 0) ; je asm_strlen@byte_0 ; Si es nulo, saltar al cálculo final para byte 0 ; test ah, ah ; Comprobar el segundo byte (byte 1) ; je asm_strlen@byte_1 ; Si es nulo, saltar al cálculo final para byte 1 ; test eax, 0x00ff0000 ; Comprobar el tercer byte (byte 2) usando máscara ; je asm_strlen@byte_2 ; Si es nulo, saltar al cálculo final para byte 2 ; test eax, 0xff000000 ; Comprobar el cuarto byte (byte 3) usando máscara ; je asm_strlen@byte_3 ; Si es nulo, saltar al cálculo final para byte 3

; ; Las siguientes etiquetas calculan la longitud final restando la dirección de inicio (guardada en ECX antes de la resta) ; ; de la dirección actual ECX (que apunta al byte después del nulo). ; ; El puntero original de inicio de la cadena se recupera del stack (después de pop ecx).

; asm_strlen@byte_3: ; El nulo estaba en la posición 3 (el byte más a la izquierda en el bloque original) ; lea eax, [ecx - 1] ; EAX = ECX - 1 (dirección del byte nulo) ; pop ecx ; Recuperar el puntero original de la cadena ; sub eax, ecx ; Calcular longitud: (dirección del nulo) - (dirección de inicio) ; jmp asm_strlen@end

; asm_strlen@byte_2: ; El nulo estaba en la posición 2 ; lea eax, [ecx - 2] ; EAX = ECX - 2 (dirección del byte nulo) ; pop ecx ; sub eax, ecx ; jmp asm_strlen@end

; asm_strlen@byte_1: ; El nulo estaba en la posición 1 ; lea eax, [ecx - 3] ; EAX = ECX - 3 (dirección del byte nulo) ; pop ecx ; sub eax, ecx ; jmp asm_strlen@end

; asm_strlen@byte_0: ; El nulo estaba en la posición 0 (el byte menos significativo) ; lea eax, [ecx - 4] ; EAX = ECX - 4 (dirección del byte nulo) ; pop ecx ; sub eax, ecx ; ; No se necesita jmp aquí ya que es la última ruta antes de @end

; asm_strlen@end: ; Limpieza del stack y retorno ; pop edx ; pop ecx ; El puntero original ya se recuperó en las rutas de byte ; pop ebp ; ret ; } ; }

; Versión ligeramente reescrita del ensamblador para mayor claridad ; y para asegurar la correcta recuperación del puntero de inicio.

int __declspec(naked)asm_strlen_revised(char* buf) { __asm { push ebp mov ebp, esp push esi ; Guardar ESI (se usará para el puntero de inicio) push edi ; Guardar EDI (se usará para el puntero de trabajo) push ebx ; Guardar EBX

    mov esi, [ebp + 8] ; Copiar el puntero de inicio de la cadena a ESI
    mov edi, esi       ; Copiar a EDI para el procesamiento

    test edi, 3        ; Comprobar alineación de 4 bytes
    je main_loop       ; Si está alineado, ir al bucle principal

misaligned_start:
    movzx eax, byte ptr [edi] ; Cargar byte actual
    inc edi                   ; Avanzar puntero
    test al, al               ; Comprobar si es nulo
    jz found_null_byte        ; Si es nulo, ir a la lógica de cálculo

    test edi, 3               ; Re-comprobar alineación
    jne misaligned_start      ; Si aún no está alineado, seguir procesando byte a byte

main_loop:
    ; Asegurarse de que EDI esté alineado antes de cargar DWORD
    ; Este bloque asume que si llegamos aquí, EDI ya está alineado o se ha alineado
    ; en el bloque misaligned_start. La comprobación `test edi, 3` en misaligned_start
    ; garantiza que si salimos de allí, EDI apunta a una posición alineada.

    mov ebx, dword ptr [edi]  ; Cargar 4 bytes (DWORD)
    add edi, 4                ; Avanzar puntero en 4 bytes

    ; Lógica de detección de nulo en el bloque de 4 bytes
    mov eax, ebx              ; Copiar valor a EAX
    add eax, 0x7efefeff       ; Suma
    xor ebx, 0xffffffff       ; Invertir bits de EBX (original_ebx)
    xor ebx, eax              ; XOR con el valor modificado
    and ebx, 0x81010100       ; Aplicar máscara

    test ebx, ebx             ; Comprobar resultado
    jnz found_null_block      ; Si no es cero, se encontró un bloque con nulo

    jmp main_loop             ; Si es cero, continuar el bucle

found_null_block:
    ; Se encontró un bloque que contiene un carácter nulo.
    ; Ahora necesitamos retroceder edi para apuntar al inicio del bloque
    ; y luego procesar byte a byte para encontrar la posición exacta.
    sub edi, 4

    ; Procesar byte a byte dentro del bloque de 4 bytes
byte_scan_loop:
    movzx eax, byte ptr [edi] ; Cargar byte actual
    inc edi                   ; Avanzar puntero
    test al, al               ; Comprobar si es nulo
    jz found_null_byte        ; Si es nulo, saltar a la lógica de cálculo final
    jmp byte_scan_loop        ; Continuar escaneo si no es nulo

found_null_byte:
    ; EAX contiene el carácter nulo, EDI apunta al byte DESPUÉS del nulo.
    ; La longitud es EDI - ESI (puntero de inicio).
    sub edi, esi              ; Calcular longitud: EDI - ESI
    mov eax, edi              ; Mover longitud a EAX para retorno

    ; Restaurar registros y retornar
    pop ebx
    pop edi
    pop esi
    pop ebp
    ret
}

}


Etiquetas: C Ensamblador optimización alineación de memoria strlen

Publicado el 6-15 20:26