2012-03-23 9 views
7

Así Hace poco estaba pensando en strcpy y de nuevo a K & R donde muestran la aplicación como¿por qué vs C++ 2010 compilador producir un código de montaje diferente para función similar

while (*dst++ = *src++) ; 

Sin embargo por error me transcribí como:

while (*dst = *src) 
{ 
    src++; //technically could be ++src on these lines 
    dst++; 
} 

En cualquier caso, eso me hizo pensar si el compilador realmente produciría un código diferente para estos dos. Mi idea inicial es que deberían ser casi idénticos, ya que src y dst se están incrementando pero nunca se usaron. Pensé que el compilador sabría que no trataría de preservarlos como "variables" en el código máquina producido.

Usando Windows7 con VS 2010 C++ SP1 construyendo en el modo Release de 32 bit (/ O2), obtuve el código de ensamblaje para las dos encarnaciones anteriores. Para evitar que la función haga referencia a la entrada directamente y esté en línea hice una dll con cada una de las funciones. He omitido el prólogo y epílogo del ASM producido.

while (*dst++ = *src++) 
6EBB1003 8B 55 08    mov   edx,dword ptr [src]  
6EBB1006 8B 45 0C    mov   eax,dword ptr [dst]  
6EBB1009 2B D0    sub   edx,eax    //prepare edx so that edx + eax always points to src  
6EBB100B EB 03    jmp   docopy+10h (6EBB1010h) 
6EBB100D 8D 49 00    lea   ecx,[ecx]    //looks like align padding, never hit this line 
6EBB1010 8A 0C 02    mov   cl,byte ptr [edx+eax] //ptr [edx+ eax] points to char in src :loop begin 
6EBB1013 88 08    mov   byte ptr [eax],cl  //copy char to dst 
6EBB1015 40     inc   eax     //inc src ptr 
6EBB1016 84 C9    test  cl,cl     // check for 0 (null terminator) 
6EBB1018 75 F6    jne   docopy+10h (6EBB1010h) //if not goto :loop begin 
     ; 

encima he anotado el código, esencialmente un solo bucle, sólo el 1 cheque de copia de memoria nula y 1.

Ahora veamos mi versión error:

while (*dst = *src) 
6EBB1003 8B 55 08    mov   edx,dword ptr [src] 
6EBB1006 8A 0A    mov   cl,byte ptr [edx] 
6EBB1008 8B 45 0C    mov   eax,dword ptr [dst] 
6EBB100B 88 08    mov   byte ptr [eax],cl  //copy 0th char to dst 
6EBB100D 84 C9    test  cl,cl     //check for 0 
6EBB100F 74 0D    je   docopy+1Eh (6EBB101Eh) // return if we encounter null terminator 
6EBB1011 2B D0    sub   edx,eax 
6EBB1013 8A 4C 02 01   mov   cl,byte ptr [edx+eax+1] //get +1th char :loop begin 
    { 
     src++; 
     dst++; 
6EBB1017 40     inc   eax     
6EBB1018 88 08    mov   byte ptr [eax],cl  //copy above char to dst 
6EBB101A 84 C9    test  cl,cl     //check for 0 
6EBB101C 75 F5    jne   docopy+13h (6EBB1013h) // if not goto :loop begin 
    } 

En mi versión, veo que primero copia el carbón 0th al destino, a continuación, busca nula, y luego entra finalmente el bucle donde comprueba para null de nuevo. Entonces, el bucle sigue siendo básicamente el mismo, pero ahora maneja el 0 ° carácter antes del bucle. Esto, por supuesto, será subóptimo en comparación con el primer caso.

Me pregunto si alguien sabe por qué el compilador no puede hacer el mismo código (o casi el mismo) que el primer ejemplo. ¿Es este un problema específico del compilador de ms o posiblemente con mi configuración de compilador/enlazador?


aquí está el código completo, 2 archivos (1 función reemplaza a la otra).

// in first dll project 
__declspec(dllexport) void docopy(const char* src, char* dst) 
{ 
    while (*dst++ = *src++); 
} 

__declspec(dllexport) void docopy(const char* src, char* dst) 
{ 
    while (*dst = *src) 
    { 
     ++src; 
     ++dst; 
    } 
} 


//seprate main.cpp file calls docopy 
void docopy(const char* src, char* dst); 
char* source ="source"; 
char destination[100]; 
int main() 
{ 

    docopy(source, destination); 
} 
+0

¿Podría publicar todas las piezas de código C con las que comenzó? Puede ser diferente debido a las declaraciones de src & dst, pero no podría saberlo. ¿Los preámbulos del ensamblador que eliminó son idénticos? No es necesario pegarlos si lo son. – gbulmer

+0

En ambos casos, ese es el código completo, a excepción de epílogo y prólogo. La declaración de ambas funciones es __declspec (dllexport) void docopy (char * src, char * dst) – skimon

+0

Este es un mal estilo de codificación de código, porque muchos lectores verán el '=' dentro de la expresión como un error tipográfico para "==". –

Respuesta

10

Porque en el primer ejemplo, el incremento posterior ocurre siempre, incluso si src comienza apuntando a un carácter nulo. En la misma situación inicial, el segundo ejemplo no incrementaría los punteros.

+0

Ah, entiendo lo que quieres decir y estoy dispuesto a aceptar tu respuesta. ¿No debería el compilador ser capaz de ver que dst y src siendo incrementados no hacen diferencia aquí ya que no son usados ​​excepto en el contexto de la copia y comparación en while y esencialmente hacen que la lógica sea la misma que en el primer ejemplo? – skimon

+0

Teóricamente, si 'src' y' dst' son locales, y no se usan nuevamente después del ciclo, entonces sí, el optimizador podría hacer la suposición. Tal vez un optimizador más agresivo haría eso. Por otro lado, el optimizador no tiene la 'otra' versión para comparar. Dada la segunda fuente, no hay nada que sugiera que podría agregar un incremento "adicional" para la eficiencia. – AShelly

+0

he publicado el código completo en mi mensaje original. @ AShelly, creo que tienes razón, quizás el compilador sea cauteloso por alguna razón aquí. – skimon

2

Por supuesto, el compilador tiene otras opciones. El "copy first byte then enter the loop if not 0" es lo que gcc-4.5.1 produce con -O1. Con -O2 y -O3, produce

.LFB0: 
    .cfi_startproc 
    jmp  .L6    // jump to copy 
    .p2align 4,,10 
    .p2align 3 
.L4: 
    addq $1, %rdi  // increment pointers 
    addq $1, %rsi 
.L6:      // copy 
    movzbl (%rdi), %eax // get source byte 
    testb %al, %al  // check for 0 
    movb %al, (%rsi)  // move to dest 
    jne  .L4    // loop if nonzero 
    rep 
    ret 
    .cfi_endproc 

que es bastante similar a lo que produce para el K & bucle R. Si eso es realmente mejor, no puedo decirlo, pero se ve mejor.

Aparte del salto en el bucle, las instrucciones para el K & bucle R son exactamente lo mismo, solo en un orden diferente:

.LFB0: 
    .cfi_startproc 
    .p2align 4,,10 
    .p2align 3 
.L2: 
    movzbl (%rdi), %eax // get source byte 
    addq $1, %rdi  // increment source pointer 
    movb %al, (%rsi)  // move byte to dest 
    addq $1, %rsi  // increment dest pointer 
    testb %al, %al  // check for 0 
    jne  .L2    // loop if nonzero 
    rep 
    ret 
    .cfi_endproc 
+0

Interesante, si compilo en 64 bits en gcc-4.4.4 veo el mismo comportamiento que usted describió. Sin embargo, en 32 bits (-m32 -O3) produce el "primer byte de copia luego ingrese el bucle si no es 0". ¿Posiblemente la disponibilidad de más registros en 64 bits hace una diferencia aquí? – skimon

+0

Buena llamada. De hecho, con -m32, 4.5.1 también copia el primer byte fuera del ciclo, y usa '% eax' como un desplazamiento,' movzbl 1 (% ecx,% eax),% edx; movb% dl, 1 (% ebx,% eax); addl 1,% eax' en lugar de incrementar los punteros directamente como en el modo de 64 bits. ¿Puedes verificar qué código produce el compilador VS para 64 bits? Si eso se comporta de manera similar, es muy probable que sea una cosa de la arquitectura. –

+0

que acaba de comprobar con 64 bits en VS, todavía hace la "copia del primer byte y luego introduce el bucle si no es 0". – skimon

0

Su segundo código no "comprobar NULL de nuevo". En su segunda versión, el cuerpo del ciclo trabaja con los caracteres en la dirección edx+eax+1 (observe la parte +1), que serían los caracteres número 1, 2, 3 y así sucesivamente.El código de prólogo funciona con el número de carácter 0. Eso significa que el código nunca verifica el mismo carácter dos veces, como parece creer. No hay "otra vez" allí.

El segundo código es un bot más enrevesado (la primera iteración del ciclo se saca efectivamente de él) ya que, como ya se ha explicado, su funcionalidad es diferente. Los valores finales de los punteros difieren entre su puño y su segunda versión.

Cuestiones relacionadas