Le reclamé a un compañero de trabajo que if (i < input.size() - 1) print(0);se optimizaría en este ciclo para que input.size()no se lea en cada iteración, ¡pero resulta que este no es el caso!
void print(int x) {
std::cout << x << std::endl;
}
void print_list(const std::vector<int>& input) {
int i = 0;
for (size_t i = 0; i < input.size(); i++) {
print(input[i]);
if (i < input.size() - 1) print(0);
}
}
De acuerdo con el Explorador de compiladores con opciones de gcc, -O3 -fno-exceptions¡en realidad estamos leyendo input.size()cada iteración y usándola leapara realizar una resta!
movq 0(%rbp), %rdx
movq 8(%rbp), %rax
subq %rdx, %rax
sarq $2, %rax
leaq -1(%rax), %rcx
cmpq %rbx, %rcx
ja .L35
addq $1, %rbx
Curiosamente, en Rust se produce esta optimización. Parece que ise reemplaza con una variable jque se reduce cada iteración, y la prueba i < input.size() - 1se reemplaza con algo así j > 0.
fn print(x: i32) {
println!("{}", x);
}
pub fn print_list(xs: &Vec<i32>) {
for (i, x) in xs.iter().enumerate() {
print(*x);
if i < xs.len() - 1 {
print(0);
}
}
}
En el Explorador del compilador, el ensamblaje relevante se ve así:
cmpq %r12, %rbx
jae .LBB0_4
Lo comprobé y estoy bastante seguro de que r12es xs.len() - 1y rbxes el contador. Anteriormente hay un addfor rbxy un movfuera del bucle en r12.
¿Por qué es esto? Parece que si GCC puede en línea size()y, operator[]como lo hizo, debería saber que eso size()no cambia. ¿Pero quizás el optimizador de GCC considera que no vale la pena convertirlo en una variable? O tal vez exista algún otro efecto secundario posible que lo haga inseguro. ¿Alguien sabe?

printlnes probablemente un método complejo, el compilador puede tener problemas para probar queprintlnno muta el vector.cout.operator<<(). El compilador no sabe que esta función de recuadro negro no obtiene una referenciastd::vectorde un global.printlnooperator<<es clave.Respuestas:
La llamada a la función no en línea
cout.operator<<(int)es un cuadro negro para el optimizador (porque la biblioteca está escrita en C ++ y todo lo que el optimizador ve es un prototipo; vea la discusión en los comentarios). Tiene que suponer que cualquier memoria que pueda ser señalada por una var global ha sido modificada.(O la
std::endlllamada. Por cierto, ¿por qué forzar una descarga de color en ese punto en lugar de simplemente imprimir un'\n'?)por ejemplo, por lo que sabe,
std::vector<int> &inputes una referencia a una variable global, y una de esas llamadas a funciones modifica esa variable global . (O hay un global envector<int> *ptralguna parte, o hay una función que devuelve un puntero a astatic vector<int>en alguna otra unidad de compilación, o de alguna otra manera en que una función podría obtener una referencia a este vector sin que le pasemos una referencia a él.Si tuviera una variable local cuya dirección nunca se hubiera tomado, el compilador podría suponer que las llamadas a funciones no en línea no podrían mutarla. Porque no habría forma de que ninguna variable global mantenga un puntero a este objeto. ( Esto se llama análisis de escape ). Es por eso que el compilador puede mantener
size_t iun registro en todas las llamadas a funciones. (int ipuede optimizarse simplemente porque está sombreadosize_t iy no se usa de otra manera).Podría hacer lo mismo con un local
vector(es decir, para los punteros base, end_size y end_capacity).ISO C99 tiene una solución para este problema:
int *restrict foo. Muchas C ++ compila apoyanint *__restrict fooa prometer que la memoria a la que apuntafooes solamente accesible a través de ese puntero. Más comúnmente útil en funciones que toman 2 matrices, y desea prometer al compilador que no se superponen. Por lo tanto, puede auto-vectorizarse sin generar código para verificar eso y ejecutar un ciclo de reserva.El OP comenta:
Eso explica por qué Rust puede hacer esta optimización, pero C ++ no.
Optimizando su C ++
Obviamente, debe usar
auto size = input.size();una vez en la parte superior de su función para que el compilador sepa que es un bucle invariante. Las implementaciones de C ++ no resuelven este problema por usted, por lo que debe hacerlo usted mismo.Es posible que también deba
const int *data = input.data();izar cargas del puntero de datos desde elstd::vector<int>"bloque de control". Es lamentable que la optimización pueda requerir cambios de fuente muy poco idiomáticos.Rust es un lenguaje mucho más moderno, diseñado después de que los desarrolladores del compilador aprendieron lo que era posible en la práctica para los compiladores. Realmente también se muestra de otras maneras, incluida la exposición portátil de algunas de las cosas interesantes que las CPU pueden hacer a través de
i32.count_ones, rotar, escanear bits, etc. Es realmente tonto que ISO C ++ todavía no exponga ninguno de estos dispositivos portátiles, exceptostd::bitset::count().fuente
operator<<esos tipos de operandos; así que en Standard C ++ no es un cuadro negro y el compilador puede asumir que hace lo que dice la documentación. Tal vez quieran apoyar a los desarrolladores de bibliotecas agregando comportamientos no estándar ...coutpermite que un objeto de una clase definida por el usuario derivada destreambufse asocie con la secuencia usandocout.rdbuf. Del mismo modo,ostreamse puede asociar un objeto derivado decout.tie.thispuntero se pasa implícitamente. Esto podría suceder en la práctica ya desde el constructor. Considere este bucle simple : solo verifiqué el bucle principal gcc (deL34:ajne L34), pero definitivamente se comporta como si los miembros del vector hubieran escapado (cargándolos de la memoria en cada iteración).