¿Es la llamada de función una barrera de memoria efectiva para las plataformas modernas?

10 minutos de lectura

¿Es la llamada de funcion una barrera de memoria efectiva
mikebloch

En un código base que revisé, encontré el siguiente modismo.

void notify(struct actor_t act) {
    write(act.pipe, "M", 1);
}
// thread A sending data to thread B
void send(byte *data) {
    global.data = data;
    notify(threadB);
}
// in thread B event loop
read(this.sock, &cmd, 1);
switch (cmd) {
    case 'M': use_data(global.data);break;
    ...
}

“Espera”, le dije al autor, un miembro senior de mi equipo, “¡aquí no hay barrera de memoria! No garantizas que global.data se vaciará de la memoria caché a la memoria principal. Si el subproceso A y el subproceso B se ejecutarán en dos procesadores diferentes, este esquema podría fallar”.

El programador senior sonrió y explicó lentamente, como si le explicara a su hijo de cinco años cómo atarse los cordones de los zapatos: “Escucha, muchacho, hemos visto aquí muchos errores relacionados con subprocesos, en pruebas de alta carga y en clientes reales”. hizo una pausa para rascarse la larga barba, “pero nunca hemos tenido un error con este modismo”.

“Pero, dice en el libro…”

“¡Silencio!”, me hizo callar de inmediato, “Tal vez en teoría, no está garantizado, pero en la práctica, el hecho de que hayas usado una llamada de función es efectivamente una barrera de memoria. El compilador no reordenará la instrucción global.data = data, ya que no puede saber si alguien lo usa en la llamada de función, y la arquitectura x86 garantizará que las otras CPU vean esta parte de los datos globales para cuando el subproceso B lea el comando de la canalización. Tenga la seguridad de que tenemos muchos problemas del mundo real de los que preocuparnos. No necesitamos invertir un esfuerzo extra en problemas teóricos falsos.

“Ten por seguro, muchacho, que con el tiempo entenderás cómo separar el verdadero problema de los no-problemas del tipo Necesito obtener un doctorado”.

¿Está en lo correcto? ¿Es eso realmente un problema en la práctica (por ejemplo, x86, x64 y ARM)?

Va en contra de todo lo que aprendí, ¡pero tiene una barba larga y se ve muy inteligente!

¡Puntos extra si puedes mostrarme un fragmento de código que demuestre que está equivocado!

  • Por supuesto que tiene razón, lo sabe por experiencia. Podría haber mencionado que escribir o leer una tubería o un zócalo siempre implica bloquear el núcleo, lo que implica una barrera, pero demostrarle eso a un joven quebrantador lleva mucho tiempo.

    -Hans Passant

    22 de mayo de 2012 a las 8:33

  • @HansPassant, pero incluso esas llamadas al sistema pueden, en casos patológicos, terminar ejecutándose en un núcleo diferente al que las llamó, emitiendo la barrera de memoria en el núcleo equivocado, ¿no es así?

    – mikebloch

    22 de mayo de 2012 a las 8:44

  • @HansPassant, ¿no puede el kernel decidir mover syscall a otro subproceso para mejorar el rendimiento de vez en cuando? ¿No puede suceder esto exactamente antes de la llamada al sistema?

    – mikebloch

    22 de mayo de 2012 a las 9:02

  • Pregunta 1: ¿Una barrera de memoria “vacía el caché a la memoria principal” o simplemente garantiza que “se han realizado todas las escrituras en el caché”? ¿No se activan los mecanismos de coherencia de caché para manejar las carreras de caché entre núcleos? Pregunta 2: ¿Cuánto tiempo demora un procesador una escritura? ¿Estamos hablando de 10 instrucciones de máquina o 1000? ¿Esto está creciendo y va a seguir creciendo? Lo pregunto porque hay muchos cientos o incluso miles de instrucciones de máquina a punto de ejecutarse en esa llamada para notificar ().

    – johnnycrash

    25 mayo 2012 a las 16:23


  • the fact you used a function call is effectively a memory barrier, the compiler will not reorder the instruction global.data = data Las barreras no son para el compilador, son para el hardware.

    – desarrollador bmw

    4 de noviembre de 2015 a las 1:23


Las barreras de memoria no son solo para evitar el reordenamiento de instrucciones. Incluso si las instrucciones no se reordenan, aún pueden causar problemas con la coherencia de la memoria caché. En cuanto al reordenamiento, depende de su compilador y configuración. ICC es particularmente agresivo con la reordenación. MSVC con optimización de todo el programa también puede serlo.

Si su variable de datos compartidos se declara como volatile, a pesar de que no está en la especificación la mayoría de los compiladores generarán una variable de memoria alrededor de las lecturas y escrituras de la variable y evitarán el reordenamiento. Esta no es la forma correcta de usar volatileni para qué estaba destinado.

(Si me quedaran votos, haría +1 en su pregunta para la narración).

  • Pero, ¿es eso realmente un problema en el x86/x64. ¿Puedo escribir un programa corto que demuestre que falla? (y gracias por las amables palabras, la discusión técnica debería ser divertida).

    – mikebloch

    22 de mayo de 2012 a las 8:24


  • x86 ofrece algunas garantías con respecto a la coherencia de caché. x64 no lo hace, pero en realidad Intel se da cuenta de que los desarrolladores escribieron un código inseguro y de mala calidad para x86 y, como tal, aunque no están obligados y no está en la especificación, realizan muchas operaciones atómicamente y también realizan sincronización de caché. Sin embargo, con ARM, todas las apuestas están canceladas. Consulte esta publicación (aunque no es específica de x86) para obtener mucha más información y más narraciones irónicas: ridiculousfish.com/blog/posts/barrier.html

    – Mahmud Al-Qudsi

    22 de mayo de 2012 a las 8:27


  • ¿Acabas de decir que Intel recompensas ¿desarrolladores que escriben código incorrecto e ignoran la documentación, invirtiendo recursos de I+D para resolver el problema que ellos mismos se provocaron? ¿Probablemente en el gasto en mi y su eficiencia de CPU? Hombre, algunos días me pregunto por qué me esfuerzo tanto.

    – mikebloch

    22 de mayo de 2012 a las 8:32

  • @mike lo intentaron no con el Itanic, y todos sabemos el éxito que tuvieron allí. Luego apareció AMD y dijo “aquí hay una plataforma de 64 bits que ejecutará sus binarios x86 y ejecute su código de mierda recién recompilado para x64 sin corregir sus errores” y así nació x86_64.

    – Mahmud Al-Qudsi

    22 de mayo de 2012 a las 8:33

  • Si bien la palabra clave volatile no garantizará las barreras de memoria ni la seguridad de subprocesos, sin embargo, protegerá una aplicación de subprocesos múltiples de errores relacionados con el compilador que realiza optimizaciones incorrectas, ya que nota que las funciones de devolución de llamada de su subproceso no se llaman en ninguna parte de su código. Es poco probable que esto suceda en los compiladores modernos para x86, pero es mucho más probable en los compiladores integrados de bajo nivel.

    – Lundin

    22 mayo 2012 a las 11:00


En la práctica, una llamada de función es una compilador barrera, lo que significa que el compilador no moverá los accesos a la memoria global más allá de la llamada. Una advertencia a esto son las funciones de las que el compilador sabe algo, por ejemplo, funciones integradas, funciones en línea (¡tenga en cuenta IPO!), etc.

Entonces, en teoría, se necesita una barrera de memoria del procesador (además de una barrera del compilador) para que esto funcione. Sin embargo, dado que está llamando a lectura y escritura, que son llamadas al sistema que cambian el estado global, estoy bastante seguro de que el kernel emite barreras de memoria en algún lugar de la implementación de esas. Sin embargo, no existe tal garantía, por lo que, en teoría, necesita las barreras.

  • Entonces, en la práctica, ¿código en modo kernel == barrera de memoria? Suena razonable, y parece que el anciano tenía razón después de todo. No parece que ICC pueda reordenar el código alrededor de la llamada al sistema, ya que no sabe lo que hará el núcleo.

    – mikebloch

    22 de mayo de 2012 a las 8:27


  • @janneb sí, pero incluso esas llamadas al sistema pueden, en casos patológicos, terminar ejecutándose en un núcleo diferente al que las llamó, emitiendo la barrera de memoria en el subproceso incorrecto.

    – mikebloch

    22 de mayo de 2012 a las 8:34

  • @blaze: como traté de explicar en la segunda oración, son llamadas de función que el compilador no puede ver de alguna manera, que el compilador debe asumir que pueden tocar el estado global. Desde la perspectiva del compilador, una llamada al sistema no es diferente de, digamos, una función en una biblioteca compartida (sin información disponible más allá del prototipo de la función).

    – janneb

    22 de mayo de 2012 a las 8:37

  • Hasta donde yo sé, el compilador no puede cambiar el orden de ejecución de la instrucción más allá de la llamada, ya que hay un punto de secuencia del lenguaje C después de la evaluación del parámetro de la función pero antes de la llamada. Entonces, antes de que se llame a la función, creo que el compilador debe terminar de jugar con el reordenamiento de instrucciones.

    – Lundin

    22 de mayo de 2012 a las 11:08


  • @Lundin: Bueno, el compilador puede hacer cualquier transformación que conserve la semántica del programa original (efectos secundarios visibles externamente, por así decirlo). Entonces, si puede probar que una expresión es pura (sin efectos secundarios) o que los efectos secundarios no importan, puede ignorar el punto de secuencia y reordenar las operaciones. En la práctica, no estoy seguro de que reordenar algo más allá de una llamada de función externa (sin información sobre las funciones internas) compre mucho, por lo que no me sorprendería si los compiladores no se molestaran en hacerlo.

    – janneb

    23 de mayo de 2012 a las 6:02

La regla básica es: el compilador debe hacer el estado global aparecer para ser exactamente como lo codificó, pero si puede probar que una función dada no usa variables globales, entonces puede implementar el algoritmo de la forma que elija.

El resultado es que los compiladores tradicionales siempre trataron las funciones en otra unidad de compilación como una barrera de memoria porque no podían ver dentro de esas funciones. Cada vez más, los compiladores modernos están desarrollando estrategias de optimización de “programa completo” o “tiempo de enlace” que rompen estas barreras y voluntad hacer que un código mal escrito falle, aunque haya estado funcionando bien durante años.

Si la función en cuestión está en una biblioteca compartida, no podrá ver su interior, pero si la función está definida por el estándar C, entonces no es necesario, ya sabe lo que hace la función, por lo que también debe tener cuidado con eso. Tenga en cuenta que un compilador no reconoce una llamada al núcleo por lo que es, pero el mero hecho de insertar algo que el compilador no puede reconocer (ensamblador en línea o una llamada de función a un archivo ensamblador) creará una barrera de memoria en sí misma.

En tu caso, notify será una caja negra que el compilador no puede ver dentro (una función de biblioteca) o contendrá una barrera de memoria reconocible, por lo que lo más probable es que esté seguro.

En la práctica, usted tiene que escribir muy mal código para caer sobre esto.

En la práctica, tiene razón y en este caso específico está implícita una barrera de memoria.

Pero el punto es que si su presencia es “discutible”, el código ya es demasiado complejo y poco claro.

Realmente chicos, usen un mutex u otras construcciones adecuadas. Es la única forma segura de lidiar con subprocesos y escribir código mantenible.

Y tal vez vea otros errores, como que el código es impredecible si se llama a send() más de una vez.

¿Ha sido útil esta solución?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad