c++ performance assembly x86-64 c++17

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 instrucciones mov , mientras que get_my_optional() solo necesita un solo movabs ?

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 .