¿Cuánto es la sobrecarga de los punteros inteligentes en comparación con los punteros normales en C ++?

101

¿Cuánto es la sobrecarga de los punteros inteligentes en comparación con los punteros normales en C ++ 11? En otras palabras, ¿mi código será más lento si utilizo punteros inteligentes y, de ser así, cuánto más lento?

Específicamente, estoy preguntando sobre C ++ 11 std::shared_ptry std::unique_ptr.

Obviamente, las cosas empujadas hacia abajo en la pila serán más grandes (al menos eso creo), porque un puntero inteligente también necesita almacenar su estado interno (recuento de referencias, etc.), la pregunta realmente es, ¿cuánto va a ser esto? afectar mi rendimiento, si es que lo hace?

Por ejemplo, devuelvo un puntero inteligente de una función en lugar de un puntero normal:

std::shared_ptr<const Value> getValue();
// versus
const Value *getValue();

O, por ejemplo, cuando una de mis funciones acepta un puntero inteligente como parámetro en lugar de un puntero normal:

void setValue(std::shared_ptr<const Value> val);
// versus
void setValue(const Value *val);
Venemo
fuente
8
La única forma de saberlo es comparar su código.
Basile Starynkevitch
¿A cuál te refieres? std::unique_ptro std::shared_ptr?
stefan
10
La respuesta es 42 (en otras palabras, quién sabe, necesita perfilar su código y comprender el hardware para su carga de trabajo típica).
Nim
Su aplicación necesita hacer un uso extremo de punteros inteligentes para que sea significativa.
user2672165
El costo de usar un shared_ptr en una función de establecimiento simple es terrible y agregará una sobrecarga múltiple del 100%.
Lothar

Respuestas:

176

std::unique_ptr tiene sobrecarga de memoria solo si le proporciona algún eliminador no trivial.

std::shared_ptr siempre tiene sobrecarga de memoria para el contador de referencia, aunque es muy pequeña.

std::unique_ptr tiene sobrecarga de tiempo solo durante el constructor (si tiene que copiar el eliminador proporcionado y / o inicializar el puntero en nulo) y durante el destructor (para destruir el objeto de propiedad).

std::shared_ptrtiene sobrecarga de tiempo en el constructor (para crear el contador de referencia), en el destructor (para disminuir el contador de referencia y posiblemente destruir el objeto) y en el operador de asignación (para incrementar el contador de referencia). Debido a las garantías de seguridad de subprocesos de std::shared_ptr, estos incrementos / decrementos son atómicos, lo que agrega algo más de sobrecarga.

Tenga en cuenta que ninguno de ellos tiene una sobrecarga de tiempo en la eliminación de referencias (para obtener la referencia al objeto de propiedad), mientras que esta operación parece ser la más común para los punteros.

En resumen, hay algunos gastos generales, pero no debería ralentizar el código a menos que cree y destruya continuamente punteros inteligentes.

lisyarus
fuente
11
unique_ptrno tiene gastos generales en el destructor. Hace exactamente lo mismo que lo haría con un puntero sin formato.
R. Martinho Fernandes
6
@ R.MartinhoFernandes en comparación con el puntero sin formato en sí, tiene una sobrecarga de tiempo en el destructor, ya que el destructor del puntero sin formato no hace nada. En comparación con cómo probablemente se usaría un puntero sin formato, seguramente no tiene sobrecarga.
lisyarus
3
Vale la pena señalar que parte del costo de construcción / destrucción / asignación de shared_ptr se debe a la seguridad de los subprocesos
Joe
1
Además, ¿qué pasa con el constructor predeterminado de std::unique_ptr? Si construye a std::unique_ptr<int>, el interno int*se inicializa, nullptrle guste o no.
Martin Drozdik
1
@MartinDrozdik En la mayoría de las situaciones, también inicializaría nulo el puntero sin procesar, para verificar su nulidad más tarde, o algo así. Sin embargo, agregó esto a la respuesta, gracias.
lisyarus
26

Como ocurre con todo el rendimiento del código, el único medio realmente confiable para obtener información sólida es medir y / o inspeccionar el código de la máquina.

Dicho esto, el razonamiento simple dice que

  • Puede esperar algo de sobrecarga en las compilaciones de depuración, ya que, por ejemplo, operator->debe ejecutarse como una llamada de función para que pueda ingresar (esto a su vez se debe a la falta general de soporte para marcar clases y funciones como no depuradas).

  • Porque shared_ptrpuede esperar cierta sobrecarga en la creación inicial, ya que eso implica la asignación dinámica de un bloque de control, y la asignación dinámica es mucho más lenta que cualquier otra operación básica en C ++ (utilícela make_sharedcuando sea posible para minimizar esa sobrecarga).

  • También porque shared_ptrexiste una sobrecarga mínima para mantener un recuento de referencia, por ejemplo, cuando se pasa un shared_ptrvalor por, pero no hay tal sobrecarga para unique_ptr.

Teniendo en cuenta el primer punto anterior, cuando mida, hágalo tanto para depurar como para versiones de lanzamiento.

El comité internacional de estandarización de C ++ ha publicado un informe técnico sobre el rendimiento , pero esto fue en 2006, antes unique_ptry shared_ptrse agregó a la biblioteca estándar. Aún así, los indicadores inteligentes eran viejos en ese momento, por lo que el informe también consideró eso. Citando la parte relevante:

“Si acceder a un valor a través de un puntero inteligente trivial es significativamente más lento que acceder a él a través de un puntero ordinario, el compilador está manejando la abstracción de manera ineficiente. En el pasado, la mayoría de los compiladores tenían importantes penalizaciones por abstracción y varios compiladores actuales todavía las tienen. Sin embargo, se ha informado que al menos dos compiladores tienen penalizaciones por abstracción por debajo del 1% y otro una penalización del 3%, por lo que eliminar este tipo de gastos generales está dentro del estado de la técnica ”.

Como conjetura informada, el "bien dentro del estado de la técnica" se ha logrado con los compiladores más populares en la actualidad, a principios de 2014.

Saludos y hth. - Alf
fuente
¿Podría incluir algunos detalles en su respuesta sobre los casos que agregué a mi pregunta?
Venemo
Esto podría haber sido cierto hace 10 años o más, pero hoy en día, inspeccionar el código de la máquina no es tan útil como sugiere la persona anterior. Dependiendo de cómo se canalizan, vectorizan las instrucciones, ... y cómo el compilador / procesador maneja la especulación, en última instancia, es qué tan rápido es. Menos código de máquina no significa necesariamente un código más rápido. La única forma de determinar el rendimiento es perfilarlo. Esto puede cambiar según el procesador y también según el compilador.
Byron
Un problema que he visto es que, una vez que se utilizan shared_ptrs en un servidor, el uso de shared_ptrs comienza a proliferar y pronto shared_ptrs se convierte en la técnica de gestión de memoria predeterminada. Así que ahora ha repetido las penalizaciones por abstracción del 1-3% que se repiten una y otra vez.
Nathan Doromal
Creo que comparar una compilación de depuración es una completa y absoluta pérdida de tiempo
Paul Childs
26

Mi respuesta es diferente a las demás y realmente me pregunto si alguna vez perfilaron el código.

shared_ptr tiene una sobrecarga significativa para la creación debido a su asignación de memoria para el bloque de control (que mantiene el contador de referencias y una lista de punteros a todas las referencias débiles). También tiene una sobrecarga de memoria enorme debido a esto y al hecho de que std :: shared_ptr es siempre una tupla de 2 punteros (uno al objeto, otro al bloque de control).

Si pasa un puntero_compartido a una función como un parámetro de valor, entonces será al menos 10 veces más lento que una llamada normal y creará muchos códigos en el segmento de código para el desenrollado de la pila. Si lo pasa por referencia, obtiene una indirección adicional que también puede ser bastante peor en términos de rendimiento.

Es por eso que no debe hacer esto a menos que la función esté realmente involucrada en la gestión de la propiedad. De lo contrario, utilice "shared_ptr.get ()". No está diseñado para asegurarse de que su objeto no se elimine durante una llamada de función normal.

Si te vuelves loco y usas shared_ptr en objetos pequeños como un árbol de sintaxis abstracta en un compilador o en nodos pequeños en cualquier otra estructura gráfica, verás una gran caída de rendimiento y un gran aumento de memoria. He visto un sistema analizador que se reescribió poco después de que C ++ 14 llegara al mercado y antes de que el programador aprendiera a usar correctamente los punteros inteligentes. La reescritura fue una magnitud más lenta que el código anterior.

No es una solución milagrosa y los consejos crudos tampoco son malos por definición. Los malos programadores son malos y el mal diseño es malo. Diseñe con cuidado, diseñe con una propiedad clara en mente e intente usar shared_ptr principalmente en el límite de la API del subsistema.

Si desea obtener más información, puede ver la buena charla de Nicolai M. Josuttis sobre "El precio real de los punteros compartidos en C ++" https://vimeo.com/131189627
Profundiza en los detalles de implementación y la arquitectura de la CPU para las barreras de escritura, atómicas bloqueos, etc. una vez que escuche, nunca hablará de que esta función sea barata. Si solo desea una prueba de la magnitud más lenta, omita los primeros 48 minutos y observe cómo ejecuta un código de ejemplo que se ejecuta hasta 180 veces más lento (compilado con -O3) cuando usa un puntero compartido en todas partes.

Lothar
fuente
¡Gracias por tu respuesta! ¿En qué plataforma te perfilaste? ¿Puede respaldar sus afirmaciones con algunos datos?
Venemo
No tengo número para mostrar, pero puede encontrar algunos en Nico Josuttis talk vimeo.com/131189627
Lothar
6
¿Has oído hablar alguna vez std::make_shared()? Además, encuentro que las demostraciones de mal uso descarado son un poco aburridas ...
Desduplicador
2
Todo lo que "make_shared" puede hacer es protegerlo de una asignación adicional y brindarle un poco más de ubicación de caché si el bloque de control se asigna frente al objeto. No puede ayudar en absoluto cuando pasa el puntero. Ésta no es la raíz de los problemas.
Lothar
14

En otras palabras, ¿mi código será más lento si utilizo punteros inteligentes y, de ser así, cuánto más lento?

¿Más lento? Lo más probable es que no, a menos que esté creando un índice enorme usando shared_ptrs y no tenga suficiente memoria hasta el punto en que su computadora comience a arrugarse, como una anciana que cae al suelo por una fuerza insoportable desde lejos.

Lo que haría que su código fuera más lento son las búsquedas lentas, el procesamiento de bucle innecesario, grandes copias de datos y muchas operaciones de escritura en el disco (como cientos).

Las ventajas de un puntero inteligente están todas relacionadas con la gestión. ¿Pero es necesaria la sobrecarga? Esto depende de su implementación. Digamos que está iterando sobre una matriz de 3 fases, cada fase tiene una matriz de 1024 elementos. Crear un smart_ptrpara este proceso puede ser exagerado, ya que una vez que finalice la iteración, sabrá que debe borrarlo. Por lo tanto, podría obtener memoria adicional al no usar un smart_ptr...

¿Pero realmente quieres hacer eso?

Una sola fuga de memoria podría hacer que su producto tenga un punto de falla en el tiempo (digamos que su programa pierde 4 megabytes cada hora, tomaría meses romper una computadora, sin embargo, se romperá, lo sabe porque la fuga está ahí) .

Es como decir "tu software está garantizado por 3 meses, entonces llámame para servicio".

Entonces, al final, realmente es una cuestión de ... ¿puedes manejar este riesgo? ¿Vale la pena perder el control de la memoria usando un puntero sin procesar para manejar su indexación sobre cientos de objetos diferentes?

Si la respuesta es sí, utilice un puntero sin formato.

Si ni siquiera quiere considerarlo, a smart_ptres una solución buena, viable e increíble.

Claudiordgz
fuente
4
ok, pero valgrind es bueno para verificar posibles fugas de memoria, por lo que siempre que lo use debe estar seguro ™
graywolf
@Paladin Sí, si puedes manejar tu memoria, smart_ptrson realmente útiles para equipos grandes
Claudiordgz
3
Yo uso unique_ptr, simplifica muchas cosas, pero no me gusta shared_ptr, el recuento de referencias no es GC muy eficiente y tampoco es perfecto
graywolf
1
@Paladin Intento usar punteros sin procesar si puedo encapsular todo. Si es algo que estaré pasando por todos lados como un argumento, entonces tal vez consideraré un smart_ptr. La mayoría de mis unique_ptrs se utilizan en la gran implementación, como un método principal o de ejecución
Claudiordgz
@Lothar Veo que parafraseaste una de las cosas que dije en tu respuesta: Thats why you should not do this unless the function is really involved in ownership management... gran respuesta, gracias, upvoted
Claudiordgz
0

Solo para echar un vistazo y solo para el []operador, es ~ 5 veces más lento que el puntero en bruto como se demuestra en el siguiente código, que se compiló utilizando gcc -lstdc++ -std=c++14 -O0y generó este resultado:

malloc []:     414252610                                                 
unique []  is: 2062494135                                                
uq get []  is: 238801500                                                 
uq.get()[] is: 1505169542
new is:        241049490 

Estoy empezando a aprender c ++, tengo esto en mente: siempre necesitas saber qué estás haciendo y tomarte más tiempo para saber lo que otros habían hecho en tu c ++.

EDITAR

Como mencionó @Mohan Kumar, proporcioné más detalles. La versión gcc es 7.4.0 (Ubuntu 7.4.0-1ubuntu1~14.04~ppa1), El resultado anterior se obtuvo cuando -O0se usa, sin embargo, cuando uso la bandera '-O2', obtengo esto:

malloc []:     223
unique []  is: 105586217
uq get []  is: 71129461
uq.get()[] is: 69246502
new is:        9683

Luego cambió a clang version 3.9.0, -O0fue:

malloc []:     409765889
unique []  is: 1351714189
uq get []  is: 256090843
uq.get()[] is: 1026846852
new is:        255421307

-O2 estaba:

malloc []:     150
unique []  is: 124
uq get []  is: 83
uq.get()[] is: 83
new is:        54

El resultado del clang -O2es asombroso.

#include <memory>
#include <iostream>
#include <chrono>
#include <thread>

uint32_t n = 100000000;
void t_m(void){
    auto a  = (char*) malloc(n*sizeof(char));
    for(uint32_t i=0; i<n; i++) a[i] = 'A';
}
void t_u(void){
    auto a = std::unique_ptr<char[]>(new char[n]);
    for(uint32_t i=0; i<n; i++) a[i] = 'A';
}

void t_u2(void){
    auto a = std::unique_ptr<char[]>(new char[n]);
    auto tmp = a.get();
    for(uint32_t i=0; i<n; i++) tmp[i] = 'A';
}
void t_u3(void){
    auto a = std::unique_ptr<char[]>(new char[n]);
    for(uint32_t i=0; i<n; i++) a.get()[i] = 'A';
}
void t_new(void){
    auto a = new char[n];
    for(uint32_t i=0; i<n; i++) a[i] = 'A';
}

int main(){
    auto start = std::chrono::high_resolution_clock::now();
    t_m();
    auto end1 = std::chrono::high_resolution_clock::now();
    t_u();
    auto end2 = std::chrono::high_resolution_clock::now();
    t_u2();
    auto end3 = std::chrono::high_resolution_clock::now();
    t_u3();
    auto end4 = std::chrono::high_resolution_clock::now();
    t_new();
    auto end5 = std::chrono::high_resolution_clock::now();
    std::cout << "malloc []:     " <<  (end1 - start).count() << std::endl;
    std::cout << "unique []  is: " << (end2 - end1).count() << std::endl;
    std::cout << "uq get []  is: " << (end3 - end2).count() << std::endl;
    std::cout << "uq.get()[] is: " << (end4 - end3).count() << std::endl;
    std::cout << "new is:        " << (end5 - end4).count() << std::endl;
}
liqg3
fuente
He probado el código ahora, solo es un 10% lento cuando se usa el puntero único.
Mohan Kumar
8
nunca realice pruebas comparativas -O0ni depure código. La salida será extremadamente ineficiente . Siempre use al menos -O2(o -O3hoy en día porque no se realiza alguna vectorización -O2)
phuclv
1
Si tiene tiempo y desea tomar un café, tome -O4 para optimizar el tiempo de enlace y todas las pequeñas funciones de abstracción se integran y desaparecen.
Lothar
Debe incluir una freellamada en la prueba malloc, y delete[]para new (o hacer variable aestática), porque los unique_ptrs están llamando delete[]bajo el capó, en sus destructores.
RnMss