page name etiquetas ejemplos description content c++ gcc optimization clang language-lawyer

c++ - name - meta tags ejemplos



¿Se permite al compilador optimizar las asignaciones de memoria de almacenamiento dinámico? (5)

Es perfectamente permisible (pero no obligatorio ) que un compilador optimice las asignaciones en su ejemplo original, y aún más en el ejemplo EDIT1 según §1.9 del estándar, que generalmente se conoce como la regla as-if :

Se requieren implementaciones conformes para emular (solo) el comportamiento observable de la máquina abstracta como se explica a continuación:
[3 páginas de condiciones]

Una representación más legible para humanos está disponible en cppreference.com .

Los puntos relevantes son:

  • No tiene volátiles, por lo que 1) y 2) no se aplican.
  • No envía / escribe ningún dato ni solicita al usuario, por lo que 3) y 4) no se aplican. Pero incluso si lo hiciera, estarían claramente satisfechos en EDIT1 (posiblemente también en el ejemplo original, aunque desde un punto de vista puramente teórico, es ilegal ya que el flujo y la salida del programa, en teoría, difieren, pero vea dos párrafos abajo).

Una excepción, incluso una no descubierta, es un comportamiento bien definido (¡no indefinido!). Sin embargo, estrictamente hablando, en caso de que se produzcan new lanzamientos (no va a suceder, vea también el siguiente párrafo), el comportamiento observable sería diferente, tanto por el código de salida del programa como por cualquier resultado que pueda seguir más adelante en el programa.

Ahora, en el caso particular de una asignación pequeña singular, puede darle al compilador el "beneficio de la duda" de que puede garantizar que la asignación no fallará.
Incluso en un sistema bajo mucha presión de memoria, ni siquiera es posible iniciar un proceso cuando tiene menos de la granularidad de asignación mínima disponible, y el montón también se habrá configurado antes de llamar a main . Entonces, si esta asignación fallara, el programa nunca comenzaría o ya habría alcanzado un final desafortunado antes de que se llame a main .
En la medida en que suponga que el compilador lo sabe, aunque la asignación podría en teoría arrojar , es legal incluso optimizar el ejemplo original, ya que el compilador prácticamente puede garantizar que no sucederá.

<ligeramente indeciso>
Por otro lado, no está permitido (y como puede observar, un error del compilador) optimizar la asignación en su ejemplo EDIT2. El valor se consume para producir un efecto observable externamente (el código de retorno).
Tenga en cuenta que si reemplaza new (std::nothrow) int[1000] con new (std::nothrow) int[1024*1024*1024*1024ll] (¡eso es una asignación de 4TiB!), Que es - en las computadoras actuales - garantizado para fallar, todavía optimiza la llamada. En otras palabras, devuelve 1 aunque escribió un código que debe generar 0.

@Yakk planteó un buen argumento en contra de esto: siempre que no se toque la memoria, se puede devolver un puntero y no se necesita RAM real. En la medida en que incluso sería legítimo optimizar la asignación en EDIT2. No estoy seguro de quién tiene razón y quién está equivocado aquí.

Hacer una asignación de 4TiB está casi garantizado que fallará en una máquina que no tiene al menos algo como una cantidad de RAM de dos dígitos gigabytes simplemente porque el sistema operativo necesita crear tablas de páginas. Ahora, por supuesto, al estándar C ++ no le importan las tablas de páginas o lo que el sistema operativo está haciendo para proporcionar memoria, eso es cierto.

Pero, por otro lado, la suposición de "esto funcionará si no se toca la memoria" se basa exactamente en tal detalle y en algo que proporciona el sistema operativo. La suposición de que si la RAM que no se toca no es realmente necesaria solo es cierta porque el sistema operativo proporciona memoria virtual. Y eso implica que el sistema operativo necesita crear tablas de páginas (puedo fingir que no lo sé, pero eso no cambia el hecho de que confío en ello de todos modos).

Por lo tanto, creo que no es 100% correcto asumir primero uno y luego decir "pero no nos importa el otro".

Entonces, sí, el compilador puede suponer que una asignación de 4TiB es en general perfectamente posible siempre que no se toque la memoria, y puede suponer que generalmente es posible tener éxito. Incluso podría suponer que es probable que tenga éxito (incluso cuando no lo es). Pero creo que, en cualquier caso, nunca se permite suponer que algo debe funcionar cuando existe la posibilidad de una falla. Y no solo existe la posibilidad de falla, en ese ejemplo, la falla es incluso la posibilidad más probable .
</ ligeramente indeciso>

Considere el siguiente código simple que hace uso de new (soy consciente de que no hay delete[] , pero no pertenece a esta pregunta):

int main() { int* mem = new int[100]; return 0; }

¿Se permite al compilador optimizar la new llamada?

En mi investigación, g ++ (5.2.0) y Visual Studio 2015 no optimizan la new llamada, mientras que clang (3.0+) sí . Todas las pruebas se han realizado con optimizaciones completas habilitadas (-O3 para g ++ y clang, modo Release para Visual Studio).

¿No es new hacer una llamada al sistema bajo el capó, lo que hace imposible (e ilegal) que un compilador lo optimice?

EDITAR : ahora he excluido el comportamiento indefinido del programa:

#include <new> int main() { int* mem = new (std::nothrow) int[100]; return 0; }

clang 3.0 ya no optimiza eso , pero las versiones posteriores sí .

EDIT2 :

#include <new> int main() { int* mem = new (std::nothrow) int[1000]; if (mem != 0) return 1; return 0; }

el sonido metálico siempre devuelve 1 .


Esto está permitido por N3664 .

Una implementación puede omitir una llamada a una función de asignación global reemplazable (18.6.1.1, 18.6.1.2). Cuando lo hace, el almacenamiento es proporcionado por la implementación o por la extensión de la asignación de otra nueva expresión.

Esta propuesta es parte del estándar C ++ 14, por lo que en C ++ 14 el compilador puede optimizar una new expresión (incluso si puede arrojarse).

Si echas un vistazo al estado de implementación de Clang , indica claramente que implementan N3664.

Si observa este comportamiento al compilar en C ++ 11 o C ++ 03, debe completar un error.

Tenga en cuenta que antes de C ++ 14 las asignaciones de memoria dinámica son parte del estado observable del programa (aunque no puedo encontrar una referencia para eso en este momento), por lo que no se permitió una implementación conforme para aplicar la regla as-if en este caso.


La historia parece ser que el sonido metálico sigue las reglas establecidas en N3664: Clarificación de la asignación de memoria que permite al compilador optimizar las asignaciones de memoria, pero como señala Nick Lewycky :

Shafik señaló que parece violar la causalidad, pero N3664 comenzó su vida como N3433, y estoy bastante seguro de que primero escribimos la optimización y de todos modos escribimos el artículo después.

Entonces clang implementó la optimización que más tarde se convirtió en una propuesta que se implementó como parte de C ++ 14.

La pregunta básica es si esta es una optimización válida antes de N3664 , esa es una pregunta difícil. Tendríamos que ir a la regla as-if cubierta en el borrador del estándar C ++ sección 1.9 Ejecución del programa que dice ( énfasis mío ):

Las descripciones semánticas en esta Norma Internacional definen una máquina abstracta no determinizada parametrizada. Esta Norma Internacional no impone ningún requisito sobre la estructura de las implementaciones conformes. En particular, no necesitan copiar o emular la estructura de la máquina abstracta. Más bien, se requieren implementaciones conformes para emular (solo) el comportamiento observable de la máquina abstracta como se explica a continuación. 5 5

donde la nota 5 dice:

Esta disposición a veces se llama la regla "como si" , porque una implementación es libre de ignorar cualquier requisito de esta Norma Internacional siempre que el resultado sea como si el requisito hubiera sido obedecido, en la medida en que pueda determinarse a partir del comportamiento observable Del programa. Por ejemplo, una implementación real no necesita evaluar parte de una expresión si puede deducir que su valor no se usa y que no se producen efectos secundarios que afecten el comportamiento observable del programa.

Como new podría generar una excepción que tendría un comportamiento observable, ya que alteraría el valor de retorno del programa, eso parecería argumentar en contra de que la regla as-if lo permita.

Sin embargo, podría argumentarse que es un detalle de implementación cuándo lanzar una excepción y, por lo tanto, el sonido metálico podría decidir, incluso en este escenario, que no causaría una excepción y, por lo tanto, eludir la new llamada no violaría la regla de as-if .

También parece válido bajo la regla as-if para optimizar la llamada a la versión no lanzada también.

Pero podríamos tener un operador global de reemplazo nuevo en una unidad de traducción diferente que podría causar que esto afecte el comportamiento observable, por lo que el compilador tendría que tener alguna forma de demostrar que este no era el caso, de lo contrario no sería capaz de realizar esta optimización sin violar la regla como si . Las versiones anteriores de clang efectivamente se optimizaron en este caso, ya que este ejemplo de Godbolt muestra cuál fue proporcionado a través de Casey aquí , tomando este código:

#include <cstddef> extern void* operator new(std::size_t n); template<typename T> T* create() { return new T(); } int main() { auto result = 0; for (auto i = 0; i < 1000000; ++i) { result += (create<int>() != nullptr); } return result; }

y optimizándolo para esto:

main: # @main movl $1000000, %eax # imm = 0xF4240 ret

De hecho, esto parece demasiado agresivo, pero las versiones posteriores no parecen hacer esto.


Lo peor que puede suceder en su fragmento es que los new lanzamientos std::bad_alloc , que no se controlan. Lo que sucede entonces está definido por la implementación.

Dado que el mejor de los casos es no operativo y el peor de los casos no está definido, el compilador puede factorizarlos como inexistentes. Ahora, si realmente intentas atrapar la posible excepción:

int main() try { int* mem = new int[100]; return 0; } catch(...) { return 1; }

... entonces se mantiene la llamada al operator new .


Tenga en cuenta que el estándar C ++ le dice qué debe hacer un programa correcto, no cómo debe hacerlo. No puede decir nada más tarde, ya que las nuevas arquitecturas pueden surgir después de la redacción del estándar y el estándar debe ser de utilidad para ellos.

new no tiene que ser una llamada al sistema bajo el capó. Hay computadoras utilizables sin sistemas operativos y sin un concepto de llamada al sistema.

Por lo tanto, siempre que el comportamiento final no cambie, el compilador puede optimizar todo y nada. Incluyendo ese new

Hay una advertencia.
Un operador global de reemplazo nuevo podría haberse definido en una unidad de traducción diferente
En ese caso, los efectos secundarios de los nuevos podrían ser tales que no se pueden optimizar. Pero si el compilador puede garantizar que el nuevo operador no tiene efectos secundarios, como sería el caso si el código publicado es el código completo, entonces la optimización es válida.
Ese nuevo puede arrojar std :: bad_alloc no es un requisito. En este caso, cuando se optimiza nuevo, el compilador puede garantizar que no se lanzará ninguna excepción y no se producirá ningún efecto secundario.