Recientemente me topé con una comparación entre Rust y C y usan el siguiente código:
bool f(int* a, const int* b) { *a = 2; int ret = *b; *a = 3; return ret != 0; }
En Rust (mismo código, pero con la sintaxis de Rust), produce el siguiente código ensamblador:
cmp dword ptr [rsi], 0 mov dword ptr [rdi], 3 setne al ret
Mientras que con gcc produce lo siguiente:
mov DWORD PTR [rdi], 2 mov eax, DWORD PTR [rsi] mov DWORD PTR [rdi], 3 test eax, eax setne al ret
El texto afirma que la función C no puede optimizar la primera línea porque a
y b
podrían apuntar al mismo número. En Rust esto no está permitido, por lo que el compilador puede optimizarlo.
Ahora a mi pregunta:
La función toma un const int*
que es un puntero a un const int . Leí esta pregunta y dice que modificar un const int con un puntero debería dar como resultado una advertencia del compilador y el peor lanzamiento en UB.
¿Podría esta función dar como resultado un UB si lo llamo con dos punteros al mismo entero?
¿Por qué el compilador de C no puede optimizar la primera línea, suponiendo que dos punteros a la misma variable serían ilegales/UB?
La función toma una
const int*
que es un puntero a una const int.
No, const int*
no es un puntero a const int . Cualquiera que diga eso se engaña.
int*
es un puntero a un int que definitivamente no es const.
const int*
es un puntero a un int de constancia desconocida.
No hay forma de expresar la noción de un puntero a un int que definitivamente es const.
Si C fuera un lenguaje mejor diseñado, entonces const int *
sería un puntero a un const int, mutable int *
(tomando prestada una palabra clave de C++) sería un puntero a un no const int e int *
sería un puntero a un int de constancia desconocida. Eliminar los calificadores (es decir, olvidar algo sobre el tipo apuntado) sería seguro, lo contrario de C real en el que agregar el calificador const
es seguro. No he usado Rust, pero parece de ejemplos en otra respuesta que usa una sintaxis como esa.
Bjarne Stroustrup, quien introdujo const
, originalmente lo llamó readonly
, que se acerca mucho más a su significado real. int readonly*
habría dejado más claro que es el puntero el que es de solo lectura, no el objeto apuntado. El cambio de nombre a const
ha confundido a generaciones de programadores.
Cuando tengo la opción, siempre escribo foo const*
, no const foo*
, como la mejor opción después de readonly*
.
Cabe señalar que esta pregunta se refiere a la optimización en -Ofast
y cómo es incluso el caso allí.
Esencialmente, el compilador C de la función no conoce el conjunto discreto completo de direcciones que se le pueden pasar, ya que no se conoce hasta el momento del enlace/tiempo de ejecución, ya que se puede llamar a la función desde varias unidades de traducción y, por lo tanto, tiene en cuenta que manejan cualquier dirección legal a la que a
y b
puedan apuntar, y por supuesto eso incluye el caso en el que se superponen.
Por lo tanto, debe usar restrict
para indicarle que actualizar a
(que la función permite porque no es un puntero a constante, pero incluso entonces la función podría descartar const) no actualiza el valor al que apunta b
, que debe incluirse en la comparación con 0, por lo tanto, la tienda a
que sucede antes de que la comparación deba continuar, mientras que en rust la suposición predeterminada es restricta. Sin embargo, el compilador de la función sabe que *a
es lo mismo que *(a+1-1)
y, por lo tanto, no producirá 2 tiendas separadas, pero no sabe si a
o b
se superponen.
¿Por qué el Compilador de C no puede optimizar la primera línea, suponiendo que dos punteros a la misma variable serían ilegales/UB?
Debido a que no le ha indicado al compilador de C que lo haga, se le permite hacer esa suposición.
C tiene un calificador de tipo para exactamente esto llamado restrict
que significa más o menos: este puntero no se superpone con otros punteros (no exactamente , pero sigue el juego).
La salida del ensamblaje para
bool f(int* restrict a, const int* b) { *a = 2; int ret = *b; *a = 3; return ret != 0; }
es
mov eax, DWORD PTR [rsi] mov DWORD PTR [rdi], 3 test eax, eax setne al ret
... que elimina/optimiza la asignación *a = 2
De https://en.wikipedia.org/wiki/Restrict
En el lenguaje de programación C, restrict es una palabra clave que se puede usar en declaraciones de puntero. Al agregar este calificador de tipo, un programador sugiere al compilador que durante la vida útil del puntero, solo se usará el puntero en sí o un valor directamente derivado de él (como puntero + 1) para acceder al objeto al que apunta.
La función int f(int *a, const int *b);
promete no cambiar el contenido de b
a través de ese puntero ... No promete nada con respecto al acceso a las variables a través del puntero a
.
Si a
y b
apuntan al mismo objeto, cambiarlo a través a
es legal (siempre que el objeto subyacente sea modificable, por supuesto).
Ejemplo:
int val = 0; f(&val, &val);
Si bien las otras respuestas mencionan el lado C, aún vale la pena echar un vistazo al lado Rust. Con Rust, el código que tienes es probablemente este:
fn f(a:&mut i32, b:&i32)->bool{ *a = 2; let ret = *b; *a = 3; return ret != 0; }
La función acepta dos referencias, una mutable y otra no. Las referencias son punteros que se garantiza que son válidos para lecturas, y también se garantiza que las referencias mutables son únicas, por lo que se optimizan para
cmp dword ptr [rsi], 0 mov dword ptr [rdi], 3 setne al ret
Sin embargo, Rust también tiene punteros en bruto que son equivalentes a los punteros de C y no ofrecen tales garantías. La siguiente función, que toma punteros sin procesar:
unsafe fn g(a:*mut i32, b:*const i32)->bool{ *a = 2; let ret = *b; *a = 3; return ret != 0; }
pierde la optimización y compila a esto:
mov dword ptr [rdi], 2 cmp dword ptr [rsi], 0 mov dword ptr [rdi], 3 setne al ret