c++ - ¿Por qué se prefiere ''/ n'' sobre “/ n” para las secuencias de salida?
performance cout (4)
En this respuesta podemos leer que:
Supongo que hay poca diferencia entre usar
''/n''
o usar"/n"
, pero este último es una matriz de (dos) caracteres, que tiene que imprimirse carácter por carácter, para lo cual debe configurarse un bucle, que es más complejo que generar un solo carácter .
énfasis mío
Eso tiene sentido para mí.
Creo que la salida de un
const char*
requiere un bucle que probará null-terminator, que
debe
introducir más operaciones que, digamos, un simple
putchar
(no implica que
std::cout
con delegados de
char
llame a eso, es solo una simplificación para presentar un ejemplo).
Eso me convenció de usar
std::cout << ''/n'';
std::cout << '' '';
más bien que
std::cout << "/n";
std::cout << " ";
Vale la pena mencionar aquí que soy consciente de que la diferencia de rendimiento es bastante insignificante.
Sin embargo, algunos pueden argumentar que el enfoque anterior tiene la intención de pasar un solo carácter, en lugar de un literal de cadena que resultó ser un
char
largo (
dos caracteres
largos si cuenta el
''/0''
).
Últimamente he hecho una pequeña revisión de código para alguien que estaba usando este último enfoque. Hice un pequeño comentario sobre el caso y seguí adelante. El desarrollador luego me agradeció y dijo que ni siquiera había pensado en esa diferencia (centrándose principalmente en la intención). No tuvo ningún impacto (como era de esperar), pero el cambio fue adoptado.
Entonces comencé a preguntarme
cómo
es
exactamente
ese cambio significativo, así que corrí a Godbolt.
Para mi sorpresa, mostró los
siguientes resultados
cuando se probó en GCC (troncal) con los
-std=c++17 -O3
.
El ensamblado generado para el siguiente código:
#include <iostream>
void str() {
std::cout << "/n";
}
void chr() {
std::cout << ''/n'';
}
int main() {
str();
chr();
}
me sorprendió, porque parece que
chr()
realidad está generando exactamente el doble de instrucciones que
str()
:
.LC0:
.string "/n"
str():
mov edx, 1
mov esi, OFFSET FLAT:.LC0
mov edi, OFFSET FLAT:_ZSt4cout
jmp std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
chr():
sub rsp, 24
mov edx, 1
mov edi, OFFSET FLAT:_ZSt4cout
lea rsi, [rsp+15]
mov BYTE PTR [rsp+15], 10
call std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
add rsp, 24
ret
¿Porqué es eso?
¿Por qué ambos finalmente llaman a la misma función
std::basic_ostream
con el argumento
const char*
?
¿Significa que el enfoque literal de caracteres no solo
no es mejor
, sino que en realidad es
peor
que el literal de cadena?
En lugar de ''/ n'', no se recomienda usar std :: endl para mayor legibilidad.
Ninguna de las otras respuestas explica realmente por qué el compilador genera el código que hace en su enlace Godbolt, por lo que pensé en incorporarlo.
Si observa el código generado, puede ver que:
std::cout << ''/n'';
Compila a, en efecto:
char c = ''/n'';
std::cout.operator<< (&c, 1);
y para que esto funcione, el compilador tiene que generar un marco de pila para la función
chr()
, que es de donde provienen muchas de las instrucciones adicionales.
Por otro lado, al compilar esto:
std::cout << "/n";
el compilador puede optimizar
str()
para simplemente ''
operator<< (const char *)
cola''
operator<< (const char *)
, lo que significa que no se necesita un marco de pila.
Por lo tanto, sus resultados están algo sesgados por el hecho de que coloca las llamadas al
operator<<
en funciones separadas.
Es más revelador hacer estas llamadas en línea, consulte:
https://godbolt.org/z/OO-8dS
Ahora puede ver que, aunque la salida
''/n''
sigue siendo un poco más costosa (porque no hay una sobrecarga específica para
ofstream::operator<< (char)
), la diferencia es menos marcada que en su ejemplo.
Sí, para esta implementación particular, por ejemplo, la versión de
char
es un poco más lenta que la versión de cadena.
Ambas versiones llaman a una función de estilo de
write(buffer, bufferSize)
.
Para la versión de cadena,
bufferSize
se conoce en tiempo de compilación (1 byte), por lo que no es necesario encontrar el tiempo de ejecución del terminador cero.
Para la versión
char
, el compilador crea un pequeño búfer de 1 byte en la pila, coloca el carácter en él y pasa este búfer para escribir.
Entonces, la versión
char
es un poco más lenta.
Sin embargo, tenga en cuenta que lo que ve en el ensamblado es solo la creación de la pila de llamadas, no la ejecución de la función real.
std::cout << ''/n'';
sigue siendo
mucho
más rápido que
std::cout << "/n";
Creé este pequeño programa para medir el rendimiento y es
aproximadamente 20 veces un
poco más rápido en mi máquina con g ++ -O3.
¡Inténtalo tú mismo!
Editar: Lo siento, noté error tipográfico en mi programa y no es mucho más rápido. Apenas puede medir alguna diferencia más. A veces uno es más rápido. Otras veces el otro.
#include <chrono>
#include <iostream>
class timer {
private:
decltype(std::chrono::high_resolution_clock::now()) begin, end;
public:
void
start() {
begin = std::chrono::high_resolution_clock::now();
}
void
stop() {
end = std::chrono::high_resolution_clock::now();
}
template<typename T>
auto
duration() const {
return std::chrono::duration_cast<T>(end - begin).count();
}
auto
nanoseconds() const {
return duration<std::chrono::nanoseconds>();
}
void
printNS() const {
std::cout << "Nanoseconds: " << nanoseconds() << std::endl;
}
};
int
main(int argc, char** argv) {
timer t1;
t1.start();
for (int i{0}; 10000 > i; ++i) {
std::cout << ''/n'';
}
t1.stop();
timer t2;
t2.start();
for (int i{0}; 10000 > i; ++i) {
std::cout << "/n";
}
t2.stop();
t1.printNS();
t2.printNS();
}
Editar: como sugirió geza, probé 100000000 iteraciones para ambos y lo envié a / dev / null y lo ejecuté cuatro veces. ''/ n'' fue una vez más lento y 3 veces más rápido pero nunca mucho, pero podría ser diferente en otras máquinas:
Nanoseconds: 8668263707
Nanoseconds: 7236055911
Nanoseconds: 10704225268
Nanoseconds: 10735594417
Nanoseconds: 10670389416
Nanoseconds: 10658991348
Nanoseconds: 7199981327
Nanoseconds: 6753044774
Supongo que en general no me importaría demasiado.