c++ - programacion - programa en c que lea un archivo txt
¿Por qué la posición de una función en un archivo c++ afecta su rendimiento? (2)
Estás llamando a
y b
de test
. Dado que el compilador no tiene motivos para reordenar sus dos funciones, a
está más lejos que b
(en el original) de la test
. También está utilizando plantillas, por lo que la generación de código real es bastante más grande de lo que parece en la fuente de C ++.
Por lo tanto, es bastante posible que la memoria de instrucciones para b
ingrese en el caché de instrucciones junto con la test
, a
ser más lejano no ingrese al caché y, por lo tanto, tome más tiempo para buscar desde cachés inferiores o memoria principal de la CPU que b
.
Por lo tanto, es posible que debido a ciclos de búsqueda de instrucciones más largos para a
que b
, a
ejecute más lentamente que b
, aunque el código real sea el mismo, está más lejos.
Ciertas arquitecturas de CPU (como la serie arm cortex-A) admiten contadores de rendimiento que cuentan el número de fallas de caché. Herramientas como perf , pueden capturar estos datos cuando están configurados para trabajar con los contadores de rendimiento apropiados.
¿Por qué la posición de una función en un archivo c ++ afecta su rendimiento? Específicamente en el ejemplo que se presenta a continuación, tenemos dos funciones idénticas que tienen perfiles de rendimiento diferentes y consistentes. ¿Cómo se investiga esto y se determina por qué el rendimiento es tan diferente?
El ejemplo es bastante sencillo ya que tenemos dos funciones: ay b. Cada uno se ejecuta muchas veces en un bucle cerrado y optimizado ( -O3 -march=corei7-avx
) y cronometrado. Aquí está el código:
#include <cstdint>
#include <iostream>
#include <numeric>
#include <boost/timer/timer.hpp>
bool array[] = {true, false, true, false, false, true};
uint32_t __attribute__((noinline)) a() {
asm("");
return std::accumulate(std::begin(array), std::end(array), 0);
}
uint32_t __attribute__((noinline)) b() {
asm("");
return std::accumulate(std::begin(array), std::end(array), 0);
}
const size_t WARM_ITERS = 1ull << 10;
const size_t MAX_ITERS = 1ull << 30;
void test(const char* name, uint32_t (*fn)())
{
std::cout << name << ": ";
for (size_t i = 0; i < WARM_ITERS; i++) {
fn();
asm("");
}
boost::timer::auto_cpu_timer t;
for (size_t i = 0; i < MAX_ITERS; i++) {
fn();
asm("");
}
}
int main(int argc, char **argv)
{
test("a", a);
test("b", b);
return 0;
}
Algunas características notables:
- Las funciones ayb son idénticas. Realizan la misma operación de acumulación y compilan las mismas instrucciones de ensamblaje.
- Cada iteración de prueba tiene un período de calentamiento antes de que comience la sincronización para tratar de eliminar cualquier problema con el calentamiento de los cachés.
Cuando esto se compila y ejecuta, obtenemos la siguiente salida que muestra a es significativamente más lento que b:
[me@host:~/code/mystery] make && ./mystery
g++-4.8 -c -g -O3 -Wall -Wno-unused-local-typedefs -std=c++11 -march=corei7-avx -I/usr/local/include/boost-1_54/ mystery.cpp -o mystery.o
g++-4.8 mystery.o -lboost_system-gcc48-1_54 -lboost_timer-gcc48-1_54 -o mystery
a: 7.412747s wall, 7.400000s user + 0.000000s system = 7.400000s CPU (99.8%)
b: 5.729706s wall, 5.740000s user + 0.000000s system = 5.740000s CPU (100.2%)
Si invertimos las dos pruebas (es decir, llamada test(b)
y luego test(a)
) a es aún más lento que b:
[me@host:~/code/mystery] make && ./mystery
g++-4.8 -c -g -O3 -Wall -Wno-unused-local-typedefs -std=c++11 -march=corei7-avx -I/usr/local/include/boost-1_54/ mystery.cpp -o mystery.o
g++-4.8 mystery.o -lboost_system-gcc48-1_54 -lboost_timer-gcc48-1_54 -o mystery
b: 5.733968s wall, 5.730000s user + 0.000000s system = 5.730000s CPU (99.9%)
a: 7.414538s wall, 7.410000s user + 0.000000s system = 7.410000s CPU (99.9%)
Si ahora invertimos la ubicación de las funciones en el archivo C ++ (movemos la definición de b sobre a), los resultados se invierten y a se vuelve más rápido que b!
[me@host:~/code/mystery] make && ./mystery
g++-4.8 -c -g -O3 -Wall -Wno-unused-local-typedefs -std=c++11 -march=corei7-avx -I/usr/local/include/boost-1_54/ mystery.cpp -o mystery.o
g++-4.8 mystery.o -lboost_system-gcc48-1_54 -lboost_timer-gcc48-1_54 -o mystery
a: 5.729604s wall, 5.720000s user + 0.000000s system = 5.720000s CPU (99.8%)
b: 7.411549s wall, 7.420000s user + 0.000000s system = 7.420000s CPU (100.1%)
Así que, en esencia, la función que se encuentra en la parte superior del archivo c ++ es más lenta.
Algunas respuestas a las preguntas que pueda tener:
- El código compilado es idéntico para a y b. El desmontaje ha sido revisado. (Para aquellos interesados: http://pastebin.com/2QziqRXR )
- El código se compiló usando gcc 4.8, gcc 4.8.1 en ubuntu 13.04, ubuntu 13.10 y ubuntu 12.04.03.
- Efectos observados en un procesador Intel Sandy Bridge i7-2600 y Intel Xeon X5482.
¿Por qué sucedería esto? ¿Qué herramientas están disponibles para investigar algo como esto?
Me parece que es un problema de alias de caché.
El caso de prueba es bastante inteligente y carga correctamente todo en el caché antes de cronometrarlo. Parece que todo encaja en el caché; aunque simulado, lo he verificado mirando la salida de la herramienta de almacenamiento de datos de valgrind, y como cabría esperar en un caso de prueba tan pequeño, no hay fallas significativas en el caché:
valgrind --tool=cachegrind --I1=32768,8,64 --D1=32768,8,64 /tmp/so
==11130== Cachegrind, a cache and branch-prediction profiler
==11130== Copyright (C) 2002-2012, and GNU GPL''d, by Nicholas Nethercote et al.
==11130== Using Valgrind-3.8.1 and LibVEX; rerun with -h for copyright info
==11130== Command: /tmp/so
==11130==
--11130-- warning: L3 cache found, using its data for the LL simulation.
a: 6.692648s wall, 6.670000s user + 0.000000s system = 6.670000s CPU (99.7%)
b: 7.306552s wall, 7.280000s user + 0.000000s system = 7.280000s CPU (99.6%)
==11130==
==11130== I refs: 2,484,996,374
==11130== I1 misses: 1,843
==11130== LLi misses: 1,694
==11130== I1 miss rate: 0.00%
==11130== LLi miss rate: 0.00%
==11130==
==11130== D refs: 537,530,151 (470,253,428 rd + 67,276,723 wr)
==11130== D1 misses: 14,477 ( 12,433 rd + 2,044 wr)
==11130== LLd misses: 8,336 ( 6,817 rd + 1,519 wr)
==11130== D1 miss rate: 0.0% ( 0.0% + 0.0% )
==11130== LLd miss rate: 0.0% ( 0.0% + 0.0% )
==11130==
==11130== LL refs: 16,320 ( 14,276 rd + 2,044 wr)
==11130== LL misses: 10,030 ( 8,511 rd + 1,519 wr)
==11130== LL miss rate: 0.0% ( 0.0% + 0.0% )
Escogí un caché asociativo de 32k y 8 vías con un tamaño de línea de 64 bytes para coincidir con las CPU Intel comunes, y vi la misma discrepancia entre las funciones a y b repetidamente.
Al ejecutarse en una máquina imaginaria con un caché asociativo de 32k y 128 vías con el mismo tamaño de línea de caché, esa diferencia desaparece:
valgrind --tool=cachegrind --I1=32768,128,64 --D1=32768,128,64 /tmp/so
==11135== Cachegrind, a cache and branch-prediction profiler
==11135== Copyright (C) 2002-2012, and GNU GPL''d, by Nicholas Nethercote et al.
==11135== Using Valgrind-3.8.1 and LibVEX; rerun with -h for copyright info
==11135== Command: /tmp/so
==11135==
--11135-- warning: L3 cache found, using its data for the LL simulation.
a: 6.754838s wall, 6.730000s user + 0.010000s system = 6.740000s CPU (99.8%)
b: 6.827246s wall, 6.800000s user + 0.000000s system = 6.800000s CPU (99.6%)
==11135==
==11135== I refs: 2,484,996,642
==11135== I1 misses: 1,816
==11135== LLi misses: 1,718
==11135== I1 miss rate: 0.00%
==11135== LLi miss rate: 0.00%
==11135==
==11135== D refs: 537,530,207 (470,253,470 rd + 67,276,737 wr)
==11135== D1 misses: 14,297 ( 12,276 rd + 2,021 wr)
==11135== LLd misses: 8,336 ( 6,817 rd + 1,519 wr)
==11135== D1 miss rate: 0.0% ( 0.0% + 0.0% )
==11135== LLd miss rate: 0.0% ( 0.0% + 0.0% )
==11135==
==11135== LL refs: 16,113 ( 14,092 rd + 2,021 wr)
==11135== LL misses: 10,054 ( 8,535 rd + 1,519 wr)
==11135== LL miss rate: 0.0% ( 0.0% + 0.0% )
Dado que en un caché de 8 vías, hay menos espacios donde las funciones de alias potenciales pueden ocultarse, se obtiene el direccionamiento equivalente a más colisiones de hash. Con la máquina que tiene una asociatividad de caché diferente, en este caso tiene la suerte de saber dónde se colocan las cosas en el archivo de objeto, y aunque no se pierda la caché, tampoco tiene que hacer ningún trabajo para resolver qué línea de caché realmente necesitar.
Edición: más sobre la asociación de caché: http://en.wikipedia.org/wiki/CPU_cache#Associativity
Otra edición: he confirmado esto con el monitoreo de eventos de hardware a través de la herramienta de perf
.
Modifiqué la fuente para llamar solo a () o b () dependiendo de si había un argumento de línea de comando presente. Los tiempos son los mismos que en el caso de prueba original.
sudo perf record -e dTLB-loads,dTLB-load-misses,dTLB-stores,dTLB-store-misses,iTLB-loads,iTLB-load-misses /tmp/so
a: 6.317755s wall, 6.300000s user + 0.000000s system = 6.300000s CPU (99.7%)
sudo perf report
4K dTLB-loads
97 dTLB-load-misses
4K dTLB-stores
7 dTLB-store-misses
479 iTLB-loads
142 iTLB-load-misses
mientras
sudo perf record -e dTLB-loads,dTLB-load-misses,dTLB-stores,dTLB-store-misses,iTLB-loads,iTLB-load-misses /tmp/so foobar
b: 4.854249s wall, 4.840000s user + 0.000000s system = 4.840000s CPU (99.7%)
sudo perf report
3K dTLB-loads
87 dTLB-load-misses
3K dTLB-stores
19 dTLB-store-misses
259 iTLB-loads
93 iTLB-load-misses
Mostrar que b tiene menos acción TLB, por lo que no es necesario desalojar el caché. Dado que la funcionalidad entre los dos es idéntica, solo se puede explicar mediante el aliasing.