c++ - ¿Por qué la construcción de std:: opcional<int> es más costosa que una std:: pair<int, bool>?
performance assembly (4)
Considere estos dos enfoques que pueden representar un "
int
opcional":
using std_optional_int = std::optional<int>;
using my_optional_int = std::pair<int, bool>;
Dadas estas dos funciones ...
auto get_std_optional_int() -> std_optional_int
{
return {42};
}
auto get_my_optional() -> my_optional_int
{
return {42, true};
}
... tanto
g ++ trunk
como
clang ++ trunk
(con
-std=c++17 -Ofast -fno-exceptions -fno-rtti
)
producen el siguiente ensamblado:
get_std_optional_int():
mov rax, rdi
mov DWORD PTR [rdi], 42
mov BYTE PTR [rdi+4], 1
ret
get_my_optional():
movabs rax, 4294967338 // == 0x 0000 0001 0000 002a
ret
ejemplo en vivo en godbolt.org
¿Por qué
get_std_optional_int()
requiere tres instrucciones
mov
, mientras que
get_my_optional()
solo necesita un solo
movabs
?
¿Es este un problema de QoI o hay algo en la especificación de
std::optional
impide esta optimización?
También tenga en cuenta que los usuarios de las funciones pueden estar completamente optimizados independientemente:
volatile int a = 0;
volatile int b = 0;
int main()
{
a = get_std_optional_int().value();
b = get_my_optional().first;
}
... resulta en:
main:
mov DWORD PTR a[rip], 42
xor eax, eax
mov DWORD PTR b[rip], 42
ret
¿Por qué
get_std_optional_int()
requiere tres instruccionesmov
, mientras queget_my_optional()
solo necesita un solomovabs
?
La causa directa es que
optional
se devuelve a través de un puntero oculto, mientras que el
pair
se devuelve en un registro.
¿Por qué es eso, sin embargo?
La especificación SysV ABI, sección
3.2.3 Paso de parámetro
dice:
Si un objeto C ++ tiene un constructor de copia no trivial o un destructor no trivial, se pasa por referencia invisible.
Ordenar el desorden de C ++ que es
optional
no es fácil, pero parece que hay un
constructor de copia no trivial al menos en la clase
optional_base
de la implementación que verifiqué
.
En
Convenciones de llamadas para diferentes compiladores de C ++ y sistemas operativos de Agner Fog
, dice que un constructor o destructor de copias evita que una estructura regrese en los registros.
Esto explica por qué
optional
no se devuelve en los registros.
Tiene que haber algo más que impida que el compilador haga la fusión de la tienda ( combina las tiendas contiguas de valores inmediatos más estrechas que una palabra en menos tiendas más anchas para reducir el número de instrucciones ) ... Actualización: error de gcc 82434 - -fstore-merging no Trabajar de manera confiable.
libstdc ++ aparentemente no implementa P0602 "la variante y opcional debe propagar la trivialidad de copiar / mover" . Puedes verificar esto con:
static_assert(std::is_trivially_copyable_v<std::optional<int>>);
que falla para libstdc ++, y pasa para libc ++ y la biblioteca estándar de MSVC (que realmente necesita un nombre propio, por lo que no tenemos que llamarlo "La implementación de MSVC de la biblioteca estándar de C ++" o "The MSVC STL").
Por supuesto, MSVC
aún
no pasará un
optional<int>
en un registro porque MS ABI.
EDITAR: Este problema se ha solucionado en la serie de lanzamientos de GCC 8.
La optimización está técnicamente permitida
, incluso con
std::is_trivially_copyable_v<std::optional<int>>
siendo falso.
Sin embargo, puede requerir un grado irrazonable de "inteligencia" para que el compilador lo encuentre.
Además, para el caso específico de usar
std::optional
como el tipo de retorno de una función, es posible que la optimización deba realizarse en tiempo de enlace en lugar de tiempo de compilación.
La realización de esta optimización no tendría ningún efecto en el comportamiento observable de ningún programa (bien definido), * y por lo tanto está implícitamente permitido bajo la regla as-if . Sin embargo, por razones que se explican en otras respuestas, el compilador no se ha dado cuenta explícitamente de este hecho y necesitaría inferirlo desde cero. El análisis estático conductual es inherentemente difícil , por lo que el compilador puede no ser capaz de demostrar que esta optimización es segura en todas las circunstancias.
Suponiendo que el compilador pueda encontrar esta optimización, entonces necesitaría alterar la convención de llamada de esta función (es decir, cambiar cómo la función devuelve un valor dado), que normalmente debe hacerse en el momento del enlace porque la convención de llamada afecta a todos los sitios de llamada. Alternativamente, el compilador podría incorporar la función por completo, lo que puede o no ser posible en el momento de la compilación. Estos pasos no serían necesarios con un objeto copiable trivialmente, por lo que en este sentido el estándar inhibe y complica la optimización.
std::is_trivially_copyable_v<std::optional<int>>
debería ser cierto.
Si fuera cierto, sería mucho más fácil para los compiladores descubrir y realizar esta optimización.
Entonces, para responder a su pregunta:
¿Es este un problema de QoI o hay algo en la especificación de
std::optional
impide esta optimización?
Son ambos. La especificación hace que la optimización sea mucho más difícil de encontrar, y la implementación no es lo suficientemente "inteligente" como para encontrarla bajo esas restricciones.
* Asumiendo que no has hecho algo realmente extraño, como
#define int something_else
.