2010-04-30 10 views
5

Estoy tratando de averiguar cuántos ciclos de reloj o instrucciones totales se necesitan para acceder a un puntero en C. No creo que sepa cómo averiguar, por ejemplo, p-> x = d-> a + f-> b¿Cuántas instrucciones para acceder al puntero en C?

supondría dos cargas por puntero, supongo que habría una carga para el puntero y una carga para el valor. Entonces, en estas operaciones, la resolución del puntero sería un factor mucho más grande que la suma real, en lo que respecta a tratar de acelerar este código, ¿verdad?

Esto puede depender del compilador y la arquitectura implementada, pero ¿estoy en el camino correcto?

he visto algo de código donde cada valor utilizado en, por ejemplo, 3 adiciones, provenía de un tipo

f2->sum = p1->p2->p3->x + p1->p2->p3->a + p1->p2->p3->m 

de la estructura, y estoy tratando de definir lo malo que es

+0

depende del modo de dirección imho - salto cercano/salto de longitud, cálculo de direcciones ... –

+0

recuerde que el compilador * debe * mover una gran cantidad de esto a la pila después de ir a buscarlo una vez. Si no es así, y no necesita preocuparse por el multihilo, puede almacenar en caché el puntero. –

+3

@Robert: si el multihilo va a afectar la desreferenciación del puntero en el ejemplo, entonces el código necesita serialización explícita: un compilador de optimización siempre podrá almacenar 'p3' en un registro y usarlo para los 3 accesos de miembros (suponiendo que haya no se usan miembros 'volátiles'). –

Respuesta

8

Esto depende la arquitectura a mano.

Algunas arquitecturas pueden hacer referencia/desviar memoria para una instrucción sin cargarla primero en un registro, otras no. Algunas arquitecturas no tienen la noción de instrucciones que calculan las compensaciones para que usted haga referencia a la desreferencia y le harán cargar la dirección de la memoria, agregarle su desplazamiento y luego permitirle quitar la referencia de la ubicación de la memoria. Estoy seguro de que hay más varianzas de chip a chip.

Una vez que haya pasado esto, cada instrucción toma una cantidad de tiempo variable dependiendo de la arquitectura también. Para ser honesto, es un gasto muy mínimo.

Para su pregunta inmediata de desreferenciar una cadena de artículos, la lentitud vendrá en el hecho de que es probable que haya una localidad de referencia pobre cuanto más lejos vaya en una cadena de desreferenciación. Esto significa más errores de caché, lo que significa más visitas a la memoria principal (¡o disco!) Para obtener los datos. La memoria principal es muy lenta en comparación con la CPU.

+2

+1 por mencionar implicaciones de caché –

+1

No creo que sea mínimo. En la optimización de código como el anterior, he visto 3 - 8x aceleraciones deshacerse de los punteros y el uso de acceso a la matriz normal. El problema es aún peor si los punteros son en realidad estructuras. – Derek

+0

@derek Bueno, ante todo, una sobrecarga potencialmente mala si el código se ejecuta constantemente, en cuyo caso, a menos que esté descartando la caché, las búsquedas continuas de memoria se deben almacenar en la DTLB (en el caso de x86). Todavía es bueno usar registros cuando sea posible, que es lo que el compilador _hace_. El ejemplo en mi respuesta muestra que puede haber acceso a un puntero incluso cuando se asignan variables locales entre sí. –

1

depende de lo que está haciendo, un puntero trivial desreferenciar y = *z; donde

int x = 1; 
int* z = &x; 
int y; 

podría montar a algo como esto en el x86:

mov eax, [z] 
mov eax, [eax] 
mov [y], eax 

y y = x habría todavía tener una falta de referencia de memoria:

mov eax, [x] 
mov [y], eax 

Instrucciones de Mov en la memoria toman alrededor de 2-4 ciclos IIRC.

Aunque, si está cargando memoria desde ubicaciones completamente aleatorias, causará una gran cantidad de fallas de página, lo que ocasionará que se desperdicien cientos de ciclos de reloj.

2

Algunos IDEs como VisualStudio le permiten ver el conjunto generado junto con el código fuente.

How to view the assembly behind the code using Visual C++?

A continuación, puede ver por su arquitectura y la aplicación exacta de lo que parece.

Si está usando GDB (Linux, Mac) utiliza disassemble

(gdb) disas 0x32c4 0x32e4 
Dump of assembler code from 0x32c4 to 0x32e4: 
0x32c4 <main+204>:  addil 0,dp 
0x32c8 <main+208>:  ldw 0x22c(sr0,r1),r26 
0x32cc <main+212>:  ldil 0x3000,r31 
0x32d0 <main+216>:  ble 0x3f8(sr4,r31) 
0x32d4 <main+220>:  ldo 0(r31),rp 
0x32d8 <main+224>:  addil -0x800,dp 
0x32dc <main+228>:  ldo 0x588(r1),r26 
0x32e0 <main+232>:  ldil 0x3000,r31 
End of assembler dump. 
+0

Compilé con la opción -S, y encontré algo muy similar a lo que otros han comentado. – Derek

1

Cuando pueda, el compilador eliminar esa sobrecarga para que al mantener posiciones de base que se usan repetidamente en un registro (por ejemplo. p1->p2->p3 en tu ejemplo).

Sin embargo, a veces el compilador no puede determinar qué indicadores podrían alias otros punteros utilizados dentro de su función - lo que significa que tiene que caer de nuevo a una posición muy conservadora, y volver a cargar los valores de los punteros frecuencia.

Aquí es donde la palabra clave restrict de C99 puede ayudar. Le permite informar al compilador cuando ciertos punteros nunca son alias por otros punteros en el alcance de la función, lo que puede mejorar la optimización.


Por ejemplo, tomemos esta función:

struct xyz { 
    int val1; 
    int val2; 
    int val3; 
}; 

struct abc { 
    struct xyz *p2; 
}; 

int foo(struct abc *p1) 
{ 
    int sum; 

    sum = p1->p2->val1 + p1->p2->val2 + p1->p2->val3; 

    return sum; 
} 

Bajo gcc 4.3.2 con nivel de optimización -O1, que recopile con este código x86:

foo: 
    pushl %ebp 
    movl %esp, %ebp 
    movl 8(%ebp), %eax 
    movl (%eax), %edx 
    movl 4(%edx), %eax 
    addl (%edx), %eax 
    addl 8(%edx), %eax 
    popl %ebp 
    ret 

Como se puede ver, solo deferencias p1 una vez - mantiene el valor de p1->p2 en el registro %edx y lo usa tres veces para obtener los tres valores de esa estructura.

+0

De hecho, escribí un programa de prueba, compilado con la opción -S, y encontré que incluso para un caso simple como p1.p2-> p3-> value o algo así, se recargaba desde p1 todo el tiempo. Muy conservador sin optimizaciones – Derek

+0

@Derek: ¿Qué nivel de optimización usó? Con '-O1' o superior, debería optimizar bastante bien los casos simples (ver el ejemplo que he agregado a mi respuesta). – caf

+0

Sí, lo hará, pero pierde parte de esa capacidad cuanto más complejo sea un programa. Este es mi punto – Derek

Cuestiones relacionadas