Rendimiento del código orientado a ADT de asignación única en CPU modernas

32

Trabajar en datos inmutables con asignaciones individuales tiene el efecto obvio de requerir más memoria, uno presumiría, porque constantemente está creando nuevos valores (aunque los compiladores debajo de las cubiertas hacen trucos de puntero para que esto sea menos problemático).

Pero he escuchado algunas veces que las pérdidas en el rendimiento son mayores que las ganancias en la forma en que la CPU (su controlador de memoria específicamente) puede aprovechar el hecho de que la memoria no está mutada (tanto).

Esperaba que alguien pudiera arrojar algo de luz sobre cómo esto es cierto (¿o si no lo es?).

En un comentario en otra publicación se mencionó que los tipos de datos abstractos (ADT) tienen que ver con esto, lo que me hizo aún más curioso, ¿cómo afectan específicamente los ADT la forma en que la CPU maneja la memoria? Sin embargo, esto es un aparte, principalmente estoy interesado en cómo la pureza del lenguaje necesariamente afecta el rendimiento de la CPU y sus cachés, etc.

Jimmy Hoffa
fuente
2
Esto es sobre todo útil en subprocesos múltiples, donde un lector puede capturar atómicamente una instantánea y estar seguro sabiendo que no mutará mientras la está leyendo
monstruo de trinquete
@ratchetfreak Entiendo que desde el punto de vista de la programación, su código tiene más garantías de seguridad, pero mi curiosidad es sobre el controlador de memoria en la CPU y cómo este comportamiento es importante (o si no es así), ya que escuché reclamos en línea sobre un puñado de veces que dijo que era más eficiente para el controlador de memoria, y no conozco los detalles de bajo nivel lo suficientemente bien como para decir si esto podría ser cierto o cómo.
Jimmy Hoffa
Incluso si fuera cierto, no creo que una modificación menor de la memoria sea el mejor punto de venta para la inmutabilidad. La memoria está allí para ser modificada, después de todo, y las CPU y los administradores de memoria se han vuelto bastante buenos a lo largo de los años.
Rein Henrichs
1
También me gustaría señalar que la eficiencia de la memoria no necesariamente depende de las optimizaciones del compilador cuando se usan estructuras inmutables. En este ejemplo let a = [1,2,3] in let b = 0:a in (a, b, (-1):c)el intercambio reduce los requisitos de memoria, sino que depende de la definición de (:)y []no el compilador. ¿Yo creo que? No estoy seguro de este.

Respuestas:

28

La CPU (su controlador de memoria específicamente) puede aprovechar el hecho de que la memoria no está mutada

La ventaja es que este hecho evita que el compilador use instrucciones membares cuando se accede a los datos.

Una barrera de memoria, también conocida como instrucción membar, cerca de memoria o cerca, es un tipo de instrucción de barrera que hace que una unidad central de procesamiento (CPU) o compilador imponga una restricción de orden en las operaciones de memoria emitidas antes y después de la instrucción de barrera. Esto generalmente significa que se garantiza que ciertas operaciones se realicen antes de la barrera y otras después.

Las barreras de memoria son necesarias porque la mayoría de las CPU modernas emplean optimizaciones de rendimiento que pueden resultar en una ejecución fuera de orden. Este reordenamiento de las operaciones de memoria (cargas y almacenes) normalmente pasa desapercibido dentro de un solo hilo de ejecución, pero puede causar un comportamiento impredecible en programas concurrentes y controladores de dispositivos a menos que se controlen cuidadosamente ...


Verá, cuando se accede a los datos desde diferentes subprocesos, en la CPU de varios núcleos se realiza de la siguiente manera: diferentes subprocesos se ejecutan en diferentes núcleos, cada uno con su propia caché (local a su núcleo), una copia de alguna caché global.

Si los datos son mutables y el programador necesita que sean consistentes entre diferentes hilos, se deben tomar medidas para garantizar la coherencia. Para el programador, esto significa usar construcciones de sincronización cuando acceden (por ejemplo, leen) a datos en un hilo particular.

Para el compilador, la construcción de sincronización en el código significa que necesita insertar una instrucción membar para asegurarse de que los cambios realizados en la copia de datos en uno de los núcleos se propaguen correctamente ("publicados"), para garantizar que los cachés en otros núcleos tener la misma copia (actualizada).

Algo más simple, vea la nota a continuación , esto es lo que sucede en el procesador multinúcleo para membar:

  1. Todos los núcleos detienen el procesamiento , para evitar escribir accidentalmente en la memoria caché.
  2. Todas las actualizaciones realizadas en las memorias caché locales se vuelven a escribir en una global, para garantizar que la memoria caché global contenga los datos más recientes. Esto lleva algo de tiempo.
  3. Los datos actualizados se vuelven a escribir del caché global a los locales, para garantizar que los cachés locales contengan los datos más recientes. Esto lleva algo de tiempo.
  4. Todos los núcleos reanudan la ejecución.

Verá, todos los núcleos no hacen nada mientras los datos se copian de un lado a otro entre cachés globales y locales . Esto es necesario para garantizar que los datos mutables estén correctamente sincronizados (seguro para subprocesos). Si hay 4 núcleos, los 4 se detienen y esperan mientras se sincronizan los cachés. Si hay 8, los 8 se detienen. Si hay 16 ... bueno, tienes 15 núcleos que no hacen exactamente nada mientras esperas las cosas que debes hacer en uno de estos.

Ahora, veamos qué sucede cuando los datos son inmutables. No importa a qué hilo acceda, se garantiza que sea el mismo. Para el programador, esto significa que no es necesario insertar construcciones de sincronización cuando acceden (leen) a los datos en un subproceso particular.

Para el compilador, esto a su vez significa que no es necesario insertar una instrucción membar .

Como resultado, el acceso a los datos no necesita detener los núcleos y esperar mientras los datos se escriben de un lado a otro entre cachés globales y locales. Esa es una ventaja del hecho de que la memoria no está mutada .


Tenga en cuenta que la explicación un poco simplificada anterior elimina algunos efectos negativos más complicados de que los datos sean mutables, por ejemplo, en la canalización . Para garantizar el pedido requerido, la CPU tiene que invalidar las líneas de pilotes afectadas por los cambios de datos; esa es otra penalización de rendimiento. Si esto se implementa mediante la invalidación directa (y por lo tanto confiable) de todas las tuberías, entonces el efecto negativo se amplifica aún más.

mosquito
fuente