¿Por qué el compilador Rust no optimiza el código suponiendo que dos referencias mutables no pueden tener alias?

301

Hasta donde yo sé, el alias de referencia / puntero puede dificultar la capacidad del compilador para generar código optimizado, ya que deben garantizar que el binario generado se comporte correctamente en el caso en que las dos referencias / punteros realmente alias. Por ejemplo, en el siguiente código C,

void adds(int  *a, int *b) {
    *a += *b;
    *a += *b;
}

cuando se compila clang version 6.0.0-1ubuntu2 (tags/RELEASE_600/final)con la -O3bandera, emite

0000000000000000 <adds>:
   0:    8b 07                    mov    (%rdi),%eax
   2:    03 06                    add    (%rsi),%eax
   4:    89 07                    mov    %eax,(%rdi)  # The first time
   6:    03 06                    add    (%rsi),%eax
   8:    89 07                    mov    %eax,(%rdi)  # The second time
   a:    c3                       retq

Aquí el código se almacena (%rdi)dos veces en mayúsculas int *ay minúsculas int *b.

Cuando explícitamente le decimos al compilador que estos dos punteros no pueden alias con la restrictpalabra clave:

void adds(int * restrict a, int * restrict b) {
    *a += *b;
    *a += *b;
}

Entonces Clang emitirá una versión más optimizada del código binario:

0000000000000000 <adds>:
   0:    8b 06                    mov    (%rsi),%eax
   2:    01 c0                    add    %eax,%eax
   4:    01 07                    add    %eax,(%rdi)
   6:    c3                       retq

Dado que Rust se asegura (excepto en un código inseguro) de que dos referencias mutables no pueden tener alias, creo que el compilador debería poder emitir la versión más optimizada del código.

Cuando pruebo con el siguiente código y lo compilo rustc 1.35.0con -C opt-level=3 --emit obj,

#![crate_type = "staticlib"]
#[no_mangle]
fn adds(a: &mut i32, b: &mut i32) {
    *a += *b;
    *a += *b;
}

genera:

0000000000000000 <adds>:
   0:    8b 07                    mov    (%rdi),%eax
   2:    03 06                    add    (%rsi),%eax
   4:    89 07                    mov    %eax,(%rdi)
   6:    03 06                    add    (%rsi),%eax
   8:    89 07                    mov    %eax,(%rdi)
   a:    c3                       retq

Esto no aprovecha la garantía ay bno puede alias.

¿Es esto porque el compilador Rust actual todavía está en desarrollo y aún no ha incorporado el análisis de alias para hacer la optimización?

¿Es esto porque todavía hay una posibilidad de que ay bpodría alias, incluso en Rust seguro?

Zhiyao
fuente
3
godbolt.org/z/aEDINX , extraño
Stargateur
76
Comentario lateral: " Dado que Rust se asegura (excepto en el código inseguro) que dos referencias mutables no pueden tener alias ", vale la pena mencionar que incluso en el unsafecódigo, no se permiten alias de referencias mutables y dan como resultado un comportamiento indefinido. Puede tener alias de punteros sin formato, pero el unsafecódigo no le permite ignorar las reglas estándar de Rust. Es solo un error común y, por lo tanto, vale la pena señalarlo.
Lukas Kalbertodt
66
Me tomó un tiempo descubrir a qué se refiere el ejemplo, porque no soy experto en leer asm, por lo que en caso de que ayude a alguien más: se reduce a si las dos +=operaciones en el cuerpo de addspueden reinterpretarse como *a = *a + *b + *b. Si los punteros no lo hacen alias, que pueden, incluso se puede ver lo que equivale a b* + *ben la segunda lista asm: 2: 01 c0 add %eax,%eax. Pero si hacen un alias, no pueden, porque para cuando agregue *bpor segunda vez, contendrá un valor diferente al de la primera vez (el que almacena en línea 4:de la primera lista de asm).
Toma el

Respuestas:

364

Rust originalmente hizo permitirá LLVM de noaliasatributo, pero este código miscompiled causado . Cuando todas las versiones de LLVM compatibles ya no compilan mal el código, se volverá a habilitar .

Si agrega -Zmutable-noalias=yesa las opciones del compilador, obtiene el ensamblado esperado:

adds:
        mov     eax, dword ptr [rsi]
        add     eax, eax
        add     dword ptr [rdi], eax
        ret

En pocas palabras, Rust puso el equivalente de la restrictpalabra clave de C en todas partes , mucho más frecuente que cualquier programa habitual de C. Esto ejerció casos de esquina de LLVM más de lo que pudo manejar correctamente. Resulta que los programadores C y C ++ simplemente no usan con restricttanta frecuencia como &mutse usa en Rust.

Esto ha sucedido varias veces .

  • Rust 1.0 a 1.7 - noaliashabilitado
  • Rust 1.8 a 1.27 - noaliasdeshabilitado
  • Rust 1.28 a 1.29 - noaliashabilitado
  • Rust 1.30 a través de ??? - noaliasdeshabilitado

Problemas relacionados con el óxido

Shepmaster
fuente
12
Esto no es sorprendente. A pesar de sus afirmaciones de amplio alcance de la compatibilidad con varios idiomas, LLVM fue diseñado específicamente como un backend de C ++ y siempre ha tenido una fuerte tendencia a ahogarse en cosas que no se parecen lo suficiente a C ++.
Mason Wheeler
47
@MasonWheeler si hace clic en algunos de los problemas, puede encontrar ejemplos de código C que usan restricty compilan mal tanto en Clang como en GCC. No se limita a lenguajes que no son "lo suficientemente C ++", a menos que cuente C ++ en ese grupo .
Shepmaster
66
@MasonWheeler: No creo que LLVM se haya diseñado realmente en torno a las reglas de C o C ++, sino más bien en torno a las reglas de LLVM. Hace suposiciones que generalmente son ciertas para el código C o C ++, pero por lo que puedo decir, el diseño se basa en un modelo de dependencias de datos estáticos que no puede manejar casos de esquina difíciles. Eso estaría bien si asumiera pesimísticamente dependencias de datos que no pueden ser refutadas, sino que se trata como acciones no operativas que escribirían almacenamiento con el mismo patrón de bits que tenía, y que tienen dependencias de datos potenciales pero no demostrables en el leer y escribir
supercat
8
@supercat He leído sus comentarios varias veces, pero admito que estoy perplejo, no tengo idea de qué tienen que ver con esta pregunta o respuesta. El comportamiento indefinido no entra en juego aquí, esto es "solo" un caso de múltiples pases de optimización que interactúan mal entre sí.
Shepmaster
2
@avl_sweden para reiterar, es solo un error . El paso de optimización de desenrollado de bucle (¿no?) No tuvo noaliasen cuenta completamente los punteros al ejecutar. Creó nuevos punteros basados ​​en punteros de entrada, copiando incorrectamente el noaliasatributo a pesar de que los nuevos punteros tenían un alias.
Shepmaster