c++ - usar - variables globales en c
¿Es más lento el acceso a una variable de función estática que el acceso a una variable global? (4)
Desde https://en.cppreference.com/w/cpp/language/initialization
Inicialización dinámica diferida
Se define por la implementación si la inicialización dinámica ocurre antes de la primera declaración de la función principal (para estadísticas) o la función inicial del subproceso (para locales de subprocesos), o aplazada para que suceda después.Si la inicialización de una variable no en línea (dado que C ++ 17) se aplaza para que suceda después de la primera declaración de la función main / thread, ocurre antes del primer uso de cualquier variable con una duración de almacenamiento de estática / thread definida en el La misma unidad de traducción que la variable a inicializar.
Por lo tanto, es posible que se deba realizar una comprobación similar para las variables globales.
por lo que f()
no es necesario "más lento" que g()
.
Las variables locales estáticas se inicializan en la primera llamada de función:
Las variables declaradas en el ámbito del bloque con el especificador estático tienen una duración de almacenamiento estático, pero se inicializan la primera vez que el control pasa a través de su declaración (a menos que su inicialización sea una inicialización cero o constante, que se puede realizar antes de ingresar el bloque por primera vez). En todas las llamadas posteriores, se omite la declaración.
Además, en C ++ 11 hay incluso más controles:
Si varios subprocesos intentan inicializar la misma variable local estática al mismo tiempo, la inicialización ocurre exactamente una vez (se puede obtener un comportamiento similar para funciones arbitrarias con std :: call_once). Nota: las implementaciones habituales de esta característica utilizan variantes del patrón de bloqueo de doble comprobación, lo que reduce la sobrecarga del tiempo de ejecución de estadísticas locales ya inicializadas a una única comparación booleana no atómica. (desde C ++ 11)
Al mismo tiempo, las variables globales parecen inicializarse en el inicio del programa (aunque técnicamente solo se menciona la asignación / desasignación en cppreference):
Duración del almacenamiento estático. El almacenamiento para el objeto se asigna cuando el programa comienza y se desasigna cuando el programa termina. Solo existe una instancia del objeto. Todos los objetos declarados en el ámbito del espacio de nombres (incluido el espacio de nombres global) tienen esta duración de almacenamiento, más los declarados con estático o externo.
Así que dado el siguiente ejemplo:
struct A {
// complex type...
};
const A& f()
{
static A local{};
return local;
}
A global{};
const A& g()
{
return global;
}
¿Tengo razón al suponer que f()
tiene que comprobar si su variable se inicializó cada vez que se llama y, por lo tanto, f()
será más lento que g()
?
Sí, es casi seguro que es un poco más lento. Sin embargo, la mayoría de las veces no importará y el costo será superado por el beneficio de "lógica y estilo".
Técnicamente, una variable estática de función local es lo mismo que una variable global. Solo que su nombre no se conoce globalmente (lo cual es bueno) y se garantiza que su inicialización sucederá no solo en un momento exacto, sino también solo una vez, y seguro para subprocesos.
Esto significa que una variable estática de función local debe saber si ha tenido lugar la inicialización y, por lo tanto, necesita al menos un acceso de memoria adicional y un salto condicional que el global (en principio) no necesita. Una implementación puede hacer algo similar para los globales, pero no es necesario (y generalmente no lo hace).
Hay muchas posibilidades de que el salto se pronostique correctamente en todos los casos, excepto en dos. Es muy probable que las dos primeras llamadas sean erróneas (por lo general, se supone que los saltos se toman en lugar de no tomarlas, las suposiciones erróneas en la primera llamada y se supone que los saltos subsiguientes toman el mismo camino que la última, otra vez mal). Después de eso, deberías estar listo para comenzar, cerca del 100% de la predicción correcta.
Pero incluso un salto pronosticado correctamente no es gratis (la CPU solo puede iniciar un número dado de instrucciones en cada ciclo, incluso suponiendo que no tardan en completarse), pero no es mucho. Si la latencia de la memoria, que puede ser un par de cientos de ciclos en el peor de los casos, se puede ocultar con éxito, el costo casi desaparece en la canalización. Además, cada acceso obtiene una línea de caché adicional que de otro modo no sería necesaria (la marca que se ha inicializado probablemente no se almacena en la misma línea de caché que los datos). Por lo tanto, tienes un rendimiento L1 ligeramente peor (L2 debería ser lo suficientemente grande para que puedas decir "sí, y qué").
También debe realizar algo una vez y ser seguro para los hilos que el global (en principio) no tiene que hacer, al menos no de la manera que se ve. Una implementación puede hacer algo diferente, pero la mayoría simplemente inicializa globales antes de que se ingrese a main
, y no es raro que la mayoría se haga con un memset
o implícitamente porque la variable se almacena en un segmento que está en cero de todos modos.
Su variable estática debe inicializarse cuando se ejecuta el código de inicialización, y debe suceder de una manera segura para subprocesos. Dependiendo de cuánto apague su implementación, esto puede ser bastante costoso. Decidí renunciar a la función de seguridad de subprocesos y siempre compilar con fno-threadsafe-statics
(incluso si esto no es compatible con el estándar) después de descubrir que GCC (que de lo contrario es un compilador completo) en realidad bloquearía un mutex para cada estática inicializacion
Usted es conceptualmente correcto, por supuesto, pero las arquitecturas contemporáneas pueden lidiar con esto.
Un compilador y una arquitectura modernos organizarían la tubería de tal manera que se asumiera la rama ya inicializada. La sobrecarga de la inicialización por lo tanto incurriría en un volcado de tubería adicional, eso es todo.
Si tiene alguna duda, compruebe el montaje.
g()
no es seguro para subprocesos y es susceptible a todo tipo de problemas de pedidos. La seguridad vendrá a un precio. Hay varias formas de pagarlo:
f()
, el Meylet Singleton, paga el precio en cada acceso. Si se accede con frecuencia o se accede durante una sección de su código sensible al rendimiento, entonces tiene sentido evitar f()
. Presumiblemente, su procesador tiene un número finito de circuitos que puede dedicar a la predicción de rama, y de todos modos se le obliga a leer una variable atómica antes de la rama. Es un precio alto que pagar continuamente por solo garantizar que la inicialización se realizó solo una vez.
h()
, que se describe a continuación, funciona de manera muy similar a g()
con una dirección indirecta adicional, pero supone que h_init()
se llama exactamente una vez al comienzo de la ejecución. Preferiblemente, definirías una subrutina que se llame como la línea de main()
; que llama a todas las funciones como h_init()
, con un orden absoluto. Con suerte, estos objetos no necesitan ser destruidos.
Alternativamente, si usa GCC, puede anotar h_init()
con __attribute__((constructor))
. Sin embargo, prefiero la explicación explícita de la subrutina init estática.
A * h_global = nullptr;
void h_init() { h_global = new A { }; }
A const& h() { return *h_global; }
h2()
es como h()
, menos la dirección indirecta adicional:
alignas(alignof(A)) char h2_global [sizeof(A)] = { };
void h2_init() { new (std::begin(h2_global)) A { }; }
A const& h2() { return * reinterpret_cast <A const *> (std::cbegin(h2_global)); }