2 puntos por GN⁺ 2024-10-03 | 1 comentarios | Compartir por WhatsApp
  • En situaciones de alta contención, las diferencias entre implementaciones de Mutex se vuelven muy evidentes, y pthread_mutex_t de Cosmopolitan Libc muestra tiempos de ejecución más cortos y menor uso de CPU que las principales implementaciones de Windows y Linux
  • En una prueba en Windows con un Threadripper 29070WX de 24 núcleos, Cosmopolitan fue 2.75 veces más rápido que Microsoft SRWLOCK y usó 18 veces menos recursos de CPU
  • En Linux con un Threadripper Pro 7995WX de 96 núcleos, es 3 veces más rápido que glibc y 11 veces más rápido que musl libc, con una brecha aún mayor en tiempo de CPU
  • En MacOS con M2 Ultra, Apple Libc queda ligeramente por delante, y Cosmopolitan usa un algoritmo simple que depende de la llamada al sistema ulock de XNU en entornos ARM
  • La base del rendimiento es la integración de nsync de Google, con una ruta rápida CAS, cola de espera, futex/ulock/WaitOnAddress(), prevención de inanición y diseño de designated waker como claves

Metodología del benchmark de Mutex con contención

  • La prueba crea 30 hilos, y cada hilo incrementa el mismo entero global g_chores 100,000 veces
  • Cada incremento se ejecuta dentro de una sección crítica muy pequeña entre pthread_mutex_lock() y pthread_mutex_unlock()
  • Las mediciones están en microsegundos y distinguen tres tiempos
    • wall time: el tiempo real que tarda en ejecutarse el programa, incluyendo la sobrecarga de creación de hilos y join
    • user time: tiempo de CPU consumido en espacio de usuario
    • system time: tiempo de CPU consumido en el kernel
  • Como varios hilos se ejecutan en paralelo, la suma de user time y system time puede ser mayor que el wall time
  • En escenarios sin contención, la diferencia de rendimiento entre implementaciones suele ser pequeña, pero en situaciones con contención las diferencias de diseño del Mutex se notan mucho

Windows: Cosmopolitan es más rápido que SRWLOCK

  • La prueba en Windows se realizó en un Threadripper 29070WX de 24 núcleos
  • MutexShootout, de Mark Waterman, evaluó SRWLOCK de Windows como la implementación más fuerte en escenarios de alta contención
  • Bajo las mismas condiciones, pthread_mutex_t de Cosmopolitan registró menor wall time y menor uso de CPU que SRWLOCK
Implementación wall time user time system time
pthread_mutex_t de Cosmopolitan 148,940µs 328,125µs 62,500µs
Microsoft SRWLOCK 410,416µs 5,515,625µs 1,640,625µs
Microsoft CRITICAL_SECTION 949,187µs 7,937,500µs 5,078,125µs
MSVC 2022 std::mutex 991,750µs 12,156,250µs 4,031,250µs
spin lock 1,165,435µs 24,515,000µs 15,000µs
pthread_mutex_t de Cygwin 9,780,803µs 1,937,000µs 6,156,000µs
  • El Mutex de Cosmopolitan es 2.75 veces más rápido que Microsoft SRWLOCK y usa 18 veces menos recursos de CPU
  • Comparado con el Mutex de Cygwin, que ofrece una implementación POSIX en Windows, es 65 veces más rápido
  • En este caso de uso, el Mutex de Cygwin resulta incluso más lento que un spin lock

Linux: una brecha de tiempo de CPU mayor que la de wall time

  • La prueba en Linux se realizó en un Threadripper Pro 7995WX de 96 núcleos
Implementación wall time user time system time
pthread_mutex_t de Cosmopolitan 36,905µs 44,511µs 23,492µs
pthread_mutex_t de glibc 101,353µs 150,706µs 2,724,851µs
spin lock 202,423µs 4,694,749µs 2,000µs
pthread_mutex_t de Musl libc 411,013µs 2,167,898µs 9,926,850µs
  • El Mutex de Cosmopolitan es 3 veces más rápido que glibc y 11 veces más rápido que musl libc
  • En términos de tiempo de CPU, usa 42 veces menos que glibc y 178 veces menos que musl libc
  • En cargas de trabajo donde todos los hilos deben realizar trabajo serializado, Cosmopolitan puede verse en htop como si solo un núcleo estuviera activo
  • En la misma situación, glibc y musl libc pueden llenar mucho el uso de CPU, lo que aumenta la carga al ejecutar varios trabajos en el mismo servidor

MacOS: Apple Libc queda apenas por delante

  • La prueba en MacOS se realizó en un M2 Ultra
Implementación wall time user time system time
Apple Libc 52,263µs 43,202µs 911,009µs
pthread_mutex_t de Cosmopolitan 54,700µs 63,055µs 1,003,674µs
  • En MacOS M2 ARM64, Apple Libc es ligeramente más rápida que el Mutex de Cosmopolitan
  • La implementación general de Mutex de Cosmopolitan no funciona bien en esta plataforma
  • En MacOS ARM, Cosmopolitan usa un algoritmo más simple basado en Futexes Are Tricky, de Ulrich Drepper
  • Este enfoque deja la mayor parte del trabajo pesado a la llamada al sistema ulock de XNU y, como resultado, logra un rendimiento casi igual al de la implementación de Apple

La base del rendimiento: integración con nsync

  • La clave del rendimiento del Mutex de Cosmopolitan es la integración de la biblioteca nsync de Google
  • nsync es una biblioteca con 371 estrellas en GitHub y fue escrita por Mike Burrows, de Google
  • Durante la integración con Cosmopolitan se realizaron los siguientes trabajos
    • Se encontró y corrigió un bug que llevaba mucho tiempo sin detectarse en la función de unlock del Mutex de nsync
    • Se portó a operaciones atómicas C11 en AARCH64, haciendo que el Mutex de nsync con contención fuera 30% más rápido que upstream nsync
    • Se reescribieron integraciones de sistema tipo futex para permitir portabilidad en runtime
    • Se hizo que funcionara de forma fluida con la cancelación de hilos POSIX

Cómo funciona nsync

  • nsync primero intenta de inmediato un CAS (compare and swap) optimista para obtener el lock rápidamente
  • Si no consigue el lock, agrega el hilo llamador a una lista doblemente enlazada de espera
    • Cada waiter tiene su propio semáforo en una línea de caché separada e independiente
    • Un hilo que entra en estado de espera ya no toca el lock principal
    • Esto es importante para reducir la sobrecarga de comunicación cuando varios núcleos tocan la misma línea de caché
    • Como contexto relacionado, se enlaza What Every Programmer Should Know About Memory, de Ulrich Drepper
  • nsync usa el futex del sistema operativo para poner hilos a dormir
    • En MacOS, el futex se llama ulock
    • En Windows, WaitOnAddress() cumple el rol de futex
    • De los sistemas operativos que soporta Cosmo, solo NetBSD no tiene futex, e implementa semáforos POSIX en espacio de kernel y requiere un nuevo descriptor de archivo por cada semáforo
  • nsync evita la inanición (starvation) con el concepto de “long wait”
    • Si un waiter se despierta 30 veces pero internamente falla cada vez al adquirir el lock, se agrega un bit al lock para impedir que hilos que aún no han esperado obtengan el lock
    • Cuando existe este bit, el CAS inicial de los hilos recién llegados falla hasta que la cola de espera se vacía hasta cierto punto
  • Los casos de uso con contención en secciones críticas pequeñas se aceleran con el concepto de designated waker
    • Cuando un hilo se despierta e intenta obtener el lock, se establece un bit en el lock principal
    • En nsync, la función de unlock tiene la responsabilidad de despertar al siguiente hilo en espera
    • Gracias a este bit, el hilo que está haciendo unlock no necesita despertar a un segundo waiter cuando ya hay un hilo despierto
  • El código fuente relacionado está en cosmopolitan/third_party/nsync/mu.c y cosmopolitan/libc/intrin/pthread_mutex_lock.c

Servicio real y código de verificación

  • Como demo en vivo que usa el Mutex de Cosmo, se puede ver el servidor http://ipv4.games/
  • Este servicio se ejecuta en una VM GCE de 2 núcleos y hasta ahora ha resistido una DDoS de botnet de hasta 49,131,669 IPs
  • Gracias a nsync, fue posible mover las consultas SQL a hilos en segundo plano y usar una estructura en la que los hilos se envían mensajes entre sí
  • Los indicadores de estado se pueden consultar en /statusz
  • El código del benchmark mide el wall time con gettimeofday() y el user time y system time con getrusage()
  • Al final, verifica que g_chores == THREADS * ITERATIONS para comprobar que se realizaron todos los incrementos

Precauciones al mirar spin locks

  • En escenarios sin contención, las diferencias entre implementaciones de Mutex son pequeñas, y un spin lock de unas pocas líneas puede ser mejor
  • Pero un spin lock solo debería usarse cuando realmente no hay otra opción
  • Es útil en lugares donde las restricciones de muy bajo nivel, como en el kernel, dificultan usar enfoques más complejos
  • También puede usarse un spin lock como detalle interno de implementación dentro de un lock de nsync
  • Si se evalúa el rendimiento de un lock solo por wall time, un spin lock puede parecer bueno, por lo que también hay que revisar el tiempo de CPU con getrusage()

1 comentarios

 
GN⁺ 2024-10-03
Opiniones de Hacker News
  • Las nuevas implementaciones de mutex y sus comparaciones siempre son interesantes, pero no me gusta este enfoque de benchmark. Parece casi un microbenchmark.
    Quienes despliegan locks rápidos en la práctica suelen usar programas multihilo muy grandes como principal medio de prueba de rendimiento. En cargas de trabajo complejas, donde varían la duración de la sección crítica, la cantidad de hilos en competencia y el grado de contención, parece que cambian los factores que hacen que un mutex sea rápido o lento.
    Como referencia, escribí el lock rápido de WebKit, inventé la abstracción ParkingLot para implementar locks (también se usa en Rust y Unreal Engine), y hace tiempo hice investigación y publiqué un artículo sobre locks rápidos para Java.

    • Desde mi experiencia creando apps de escritorio, agregaría que en una app con decenas de hilos que se ejecutan con frecuencia me gustaría ver cifras de rendimiento para el caso de baja contención.
      Como programador de audio en tiempo real, me importa más el costo de tomar un mutex que todavía no está bloqueado. En nuestra app, esa situación es por mucho la más común. Del mismo modo, también quisiera saber el costo de una operación try-lock que va a fallar, no cuando N hilos compiten.
      Como Cosmopolitan es open source, podría medirlo yo mismo, pero aun así se echa de menos.
    • Pensé lo mismo. Hay varios tipos de mutex, y algunos son mejores para cargas de trabajo específicas. Me vienen a la mente DistributedMutex y SharedMutex (https://github.com/facebook/folly/blob/main/folly/synchroniz..., https://github.com/facebook/folly/blob/main/folly/SharedMute...).
      Al igual que con los hash maps, rara vez un único hash map es mejor para todas las cargas de trabajo posibles.
    • Este estilo de mutex también se usará en PyMutex de Python 3.13. Hay benchmarks reales que muestran qué tan rápido es PyMutex frente a PyThread_type_lock anterior a 3.13.
    • Definitivamente es un microbenchmark, y es muy probable que no represente el rendimiento general. Esta página plantea buenos criterios para prácticas de benchmarking de sistemas operativos. Aunque está un poco más orientada al ámbito académico: https://gernot-heiser.org/benchmarking-crimes.html
    • Ese benchmark en particular probablemente favorezca comportamientos indeseables, por ejemplo una injusticia patológica. La planificación óptima sería ejecutar todas las operaciones de incremento del primer hilo y luego todas las del segundo, porque así se minimiza el tráfico entre procesadores.
      Un mutex que, al fallar al adquirir el lock, duerme un tiempo fijo (por ejemplo, 100 µs) casi siempre agrupará el trabajo, se acercará a ese comportamiento y podrá “ganar” en el benchmark. Pero en una aplicación real, con incluso un poco de contención, ese mutex sería terrible.
      No digo que este mutex sea malo ni que el mutex pthread sea bueno; digo que ese microbenchmark no mide algo que permita predecir el rendimiento en aplicaciones reales.
  • En la parte que dice que “Cosmopolitan Mutex es bueno porque usó una biblioteca llamada nsync”, nunca había oído hablar de nsync, pero Mike Burrows también escribió la implementación de mutex de producción de Google: https://github.com/abseil/abseil-cpp/blob/master/absl/synchr...
    Por eso me pregunto por qué esa implementación de mutex quedó fuera del benchmark. Y si en macOS delega a __ulock, parece que se podría lograr de forma más simple usando solo las funciones miembro wait() y notify_one() de la biblioteca atomic de libc++.
    Hace tiempo también hubo un hilo grande relacionado con la mejora de la implementación de mutex en Rust: https://github.com/rust-lang/rust/issues/93740#issuecomment-.... Lo interesante es que allí se discute en detalle el funcionamiento interno de casi todas las implementaciones populares de mutex.

    • Cuando entré a AV, Mike ya era una leyenda. Se decía que cada vez que el motor de búsqueda necesitaba ser más rápido, él venía, reescribía algunas funciones clave y volvía a lo que estaba haciendo antes.
      Puede que sea cierto, pero no puedo confirmarlo directamente. Era un ingeniero extremadamente inteligente y enfocado en la eficiencia. Aunque no solíamos permanecer mucho tiempo en un mismo servidor.
    • Burrows también participó en la transformación Burrows-Wheeler, Bigtable, Dapper, Chubby, etc.
    • Ese hilo de Rust llega a eso eventualmente, pero básicamente trata sobre el trabajo de Mara, y por eso también se menciona su libro publicado en enero de 2023.
      La implementación actual del mutex de Rust entró a principios de este año y, aunque quizá no sea muy distinta en Linux, entiendo que en Windows y Mac sí es trabajo nuevo.
      Aun así, la explicación de Mara sobre los interiores de otras implementaciones sigue siendo interesante, pero conviene comprobar si esa información está desactualizada para tu caso.
    • La razón por la que la implementación de mutex de Abseil quedó fuera del benchmark podría ser que es una implementación en C++, no en C. Es una suposición.
    • Parece que Mike Burrows también recibió un premio de la ACM, y allí incluso aparece en una foto.
      https://awards.acm.org/award-recipients/burrows_9434147
  • La frase “Todavía es una biblioteca C nueva y tiene partes ásperas, pero está mejorando tan rápido que no usarla en producción empieza a parecer una irresponsabilidad profesional” suena bastante rara. Valoro mucho el proyecto Cosmopolitan, pero este tipo de afirmaciones exageradas de superioridad suelen ser una señal de alerta bastante mala.

    • Creo que las afirmaciones de Justine en general son correctas. Dicho eso, parece que usar exageraciones y expresiones autopromocionales es parte de su estilo, o quizá de su personalidad.
      Entiendo que a algunas personas les pueda parecer brusco. Antes también hubo un drama de ese tipo en llamacpp.
    • Justine parece una persona bastante brillante y creativa, pero no me gustaría usar en producción una libc “nueva” y “con partes ásperas”.
      Lo más prioritario en producción no es que “mejore rapidísimo”, sino la estabilidad, previsibilidad y confiabilidad. Claro que el rendimiento también importa. Un código más rápido puede reducir infraestructura, lo cual es bueno en costos y en impacto ambiental. Pero la velocidad va al final de la lista.
    • Cuando uno pasa mucho tiempo solo programando frente a la computadora, quizá la falta de contacto social termina trayendo cierto grado de arrogancia. Si no hay mecanismos que pongan en perspectiva la importancia de uno mismo o de su trabajo, los logros, aunque sean impresionantes, pueden parecer más grandiosos de lo que han sido reconocidos ampliamente.
      Por ejemplo, APE me parece un hack muy impresionante, pero también se puede criticar diciendo: “¿Entonces ahora, en vez de ser inseguro en una sola plataforma, puede ser inseguro en varias plataformas al mismo tiempo?”.
      Cuanto más tiempo paso en tecnología, más me doy cuenta de que los beneficios mutuos perfectos son rarísimos, y que la mayoría de las cosas son trade-offs, con ganancias y pérdidas al mismo tiempo.
    • Al menos a mí me pareció una broma.
    • Me pregunto si has considerado que tú y Justine podrían tener sentidos del humor distintos. Tampoco sé a quién crees que ayuda publicar esto aquí.
  • Es totalmente tangencial, pero como desarrollador de juegos terminé apreciando los mutex lentos que hacen mucho trabajo de depuración en todas las builds de desarrollo. Por ejemplo, que tengan un nombre/ID de depuración, rastreen al dueño, reporten al profiler el tiempo consumido en contención y también reporten los cambios de propiedad.
    Los juegos tienden a estructurar la concurrencia de otra manera, y también han desarrollado patrones para evitar locks. Pero esos patrones son difíciles de usar y obligan a los programadores a cambiar la estructura. La mayor parte del código empieza con “pongamos un lock aquí por ahora y pasemos el milestone”.
    Incluso los locks rápidos pueden volverse impredeciblemente lentos y romper cualquier garantía de tiempo real que hubiera. Pueden ser rápidos en promedio, pero la latencia de cola no desaparece. No quiero ser la persona que vuelve a investigar “nuestro juego se traba”, pero normalmente termino siendo esa persona.
    Así que prefiero usar locks lentos. Esos que aparecen enormes y en rojo en el profiler. Si ves que te están pegando, los refactorizas y los eliminas.
    Sé que es una exigencia difícil. En una producción AAA, la gente que sabe usar un profiler se puede contar con los dedos. Lo he visto en varias producciones y siempre fue así.
    Perdón por la queja, pero espero que continúe la investigación en primitivas y algoritmos de concurrencia rápidos.

    • Más tangencial todavía: esta es una de las razones por las que disfruto desarrollar juegos en Rust.
      En juegos, si es posible, nunca quieres contención de locks, y en muchos casos puedes demostrar que tomar un lock es innecesario. Por ejemplo, cada frame se divide en etapas, y el acceso mutable a cierto recurso compartido solo hace falta en una etapa específica. Casos como update() antes de render(), o hot reload de assets.
      Con scoped threads y las reglas de préstamo de Rust, puedes estructurarlo de modo que no haga falta ningún mutex, y puedes tener la certeza de que, si más adelante el código cambia y pasa a ser necesario, el compilador dará un error de forma estricta.
      Siempre que se pueda, es mejor recibir un error de compilación que un pico en el profiler.
    • Totalmente de acuerdo. Las funciones de depuración, como la detección de deadlocks o la verificación del estado interno, se pagan solas fácilmente. Si estás adquiriendo un lock con tanta frecuencia como para afectar el rendimiento, deberías revisar el diseño. Hay que evitar compartir estado mutable entre threads.
  • Por un lado, la familia Cosmo/APE/redbean parece realmente impresionante, y los comentarios en los artículos relacionados en general son positivos; tampoco hay mucho que refute el concepto en sí. Pero, por otro lado, casi no he oído que otra gente lo esté usando
    No todo el mundo comparte mucho su trabajo, pero si ya pasaron varios años, esperaría haber visto al menos algunos posts de retrospectiva de proyectos. Todas las menciones a Cosmo/APE/redbean que vi salieron del sitio de Justine
    Por eso me da curiosidad. ¿Hay alguna trampa oculta? ¿Es una herramienta que hace algo malo para obtener esos resultados? ¿Es una broma o troleo al estilo tom7 que no entiendo porque no conozco a fondo compiladores o runtimes? ¿O de verdad son herramientas ingeniosas que todavía no se han difundido mucho?

    • APE funciona mediante un truco ingenioso que podría bloquearse en cualquier momento, y de hecho ya se bloqueó en OpenBSD
      La mayoría de quienes crean software multiplataforma no quieren un único ejecutable que corra en todas las plataformas, sino una única base de código que funcione correctamente en cada plataforma soportada
      Desde esa perspectiva, un lenguaje como Go, que permite compilar cruzado a todos los targets si evitas CGO, es una delicia. Pero la magia de APE para ejecutarse de tres formas, por muy ingeniosa que sea, no inspira confianza en que vaya a funcionar para siempre, y para la mayoría tampoco ofrece mucho beneficio práctico
      Cada plataforma tiene sus propios requisitos de empaquetado y firma, así que es mejor compilar por separado para cada target de plataforma
    • En lo personal, cosmo y ape me parecen muy ingeniosos, pero si las herramientas comunes ya funcionan bien, no necesito ese tipo de ingenio en el trabajo
      Por ejemplo, si ya puedes compilar cruzado un proyecto para otros sistemas operativos y plataformas, o si ya tienes esa infraestructura de build, no hay motivo para buscar una solución que produzca un único binario que funcione en todas partes
      Además, APE usa hacks ingeniosos para ejecutarse en varios sistemas operativos. ¿Qué pasa si, a medida que evolucionan los formatos de ejecutables, ese hack se rompe algún día? ¿Y si nadie tiene tiempo de arreglar APE para adaptarlo a ese cambio?
      En cambio, las herramientas aburridas como gcc, clang, go y rust se seguirán actualizando y seguirán funcionando en sistemas operativos que evolucionan. Por eso simplemente me quedo con lo aburrido. No me preocupo por lo ingenioso porque lo aburrido, para mí, simplemente funciona bien
    • llamafile de Mozilla usa esto. Empaqueta los pesos del modelo y el ejecutable en uno solo para que pueda ejecutarse en cualquier plataforma cosmo/ape, y también levanta un servidor HTTP redbean para la interacción
      También se puede ejecutar sin pesos integrados y hacer que lea los pesos desde el sistema de archivos. Puede ser la forma más fácil de “descargar y ejecutar al instante” un LLM local
    • Cosmopolitan siempre me pareció una especie de brecha técnica ideal para posts de blog entretenidos. Es el tipo de cosa que, solo por su ingenio y obsesión por la configuración, casi tiene garantizada la portada de sitios como HN
      Pero para usarlo como tecnología base, como libc, parece más útil sobre todo como juguete divertido o para pequeños proyectos personales
      En ese contexto, cuando se lo presenta como una alternativa seria a cosas como glibc, musl o msvcrt, se siente un poco raro. Es un hack muy simpático, pero si lo encontrara en algo de lo que dependo en serio, me preocuparía bastante
    • Mozilla tiene el proyecto Llamafile basado en Cosmopolitan libc: https://github.com/Mozilla-Ocho/llamafile
      También suben regularmente a Hugging Face modelos populares reempaquetados en ese formato: https://huggingface.co/models?search=llamafile
      Dicho eso, si tiene utilidad práctica más allá de probar rápidamente modelos pequeños es otra cuestión
  • Si es tan bueno, me pregunto por qué no todas las bibliotecas de C adoptaron el mismo truco.
    Mi suposición es que esos trucos probablemente solo sean consistentemente rápidos en ciertas arquitecturas, ciertos modelos de CPU, ciertas cargas de trabajo o patrones de acceso. Si se hicieran benchmarks adecuados con diversas cargas de trabajo en todo el hardware soportado, quizá no se obtendría la misma ventaja.
    O tal vez la semántica de la API pthread que Cosmopolitan intenta implementar sea sutilmente distinta, y esta implementación no cumpla estrictamente con la especificación.
    Me cuesta imaginar que varios autores de libc no estén al día con la investigación más reciente sobre primitivas del sistema operativo.

    • Esos proyectos tienen decenas de prioridades además de una API específica. Obsesionarse con una API individual no es una buena forma de usar tiempo limitado. Y, como contraejemplo, basta mirar malloc y las rutinas de cadenas en las libc comunes de Linux.
      El malloc de glibc es más o menos usable, pero en velocidad general y escalabilidad queda fácilmente por detrás de alternativas más modernas. Sufre mucha fragmentación, empeora con el tiempo y tiene muchos ajustes como MALLOC_ARENA_MAX que impactan bastante en cargas de trabajo reales. El malloc de musl es terrible en rendimiento a todos los niveles. Usar el asignador de musl en programas multihilo arruinaba tanto el rendimiento que casi podría llamarse negligencia.
      musl tampoco tiene cosas como rutinas de comparación de cadenas optimizadas con SIMD. Te sorprendería saber cuántos ciclos de CPU se gastan en estas operaciones en programas no triviales; aparece claramente en perfiles reales, y mejorarlo beneficia de forma casi universal a casi todos los programas. Las rutinas optimizadas de glibc son buenas, pero aun así parece que podrían ser más rápidas.
      Estas no son “optimizaciones especializadas para una sola arquitectura que no se generalizan”. En particular, estas dos áreas están bien exploradas y entendidas, y reducen el tiempo de reloj de pared entre 2 y 5 veces en casi cualquier carga de trabajo, además de mejorar mucho el uso del conjunto de trabajo a largo plazo. Entonces, ¿por qué no se adoptaron? Como siempre, probablemente porque había otras cosas que hacer, o porque existían prioridades en conflicto, como en musl, que prioriza la simplicidad sobre el máximo rendimiento.
      No estoy culpando a esos proyectos. Nadie dice “mi programa es horriblemente lento, está diseñado para no hacer nada bien y estoy orgulloso de eso”. Pero la idea de que quienes trabajan en esos proyectos solo eligieron diseños en una frontera de Pareto perfecta no es nada realista y no refleja cómo funcionan realmente la mayoría de los proyectos.
    • Podría deberse a política, síndrome NIH o mantenedores antiguos.
      Cambiar algo en glibc o en el equivalente del lado de C++ tarda una eternidad.
      Hay varios tipos de primitivas de sincronización, y pthreads solo soporta algunas. Si te limitas a eso, por lo general estás renunciando a rendimiento a cambio de portabilidad.
    • Me pregunto si “cuesta imaginar que varios autores de libc no estén al día con la investigación más reciente sobre primitivas del sistema operativo” era sarcasmo.
      No sé en el caso de los mantenedores de libc, pero como alguien que mantiene algunas cosas, no intento implementar la investigación más reciente. Intento mantener la estabilidad y asegurarme de que el rendimiento sea aceptable. Las implementaciones de investigación quedan fuera de mi presupuesto de “mantenimiento”.
    • Me pregunto si hay consideraciones de ABI al cambiar la implementación de mutex de pthread.
    • La pregunta “si es tan bueno, ¿por qué no todas las bibliotecas de C adoptaron el mismo truco?” me recuerda este chiste:
      Un hombre y un estadístico caminan por la calle y ven un billete de 50 euros. El estadístico sigue caminando, y el hombre se detiene y dice: “Mira, hay dinero en el suelo”. Entonces el estadístico dice: “Debe de ser falso. Si fuera real, alguien ya lo habría recogido”, y sigue caminando. El otro hombre recoge el dinero.
  • Los hilos y los mutex son de las cosas que más complejidad agregan en ciencias de la computación. Siempre miro con escepticismo una implementación nueva hasta que se haya usado a gran escala durante años.
    Los bugs en mecanismos de threading como estos muchas veces se escapan incluso a las revisiones más intensas. Cuando Java apareció a mediados de los 90, expuso todo tipo de bugs de hilos y mutex en Solaris.
    No necesitamos la implementación de mutex más rápida, sino una implementación confiable.

    • Los mutex están lejos de ser de las cosas más “complejas”. Tampoco hay tantas formas de implementarlos eficientemente. En la mayoría de los casos, especialmente en la ruta de lectura, lo mejor es evitarlos.
  • Este código no hace benchmark del rendimiento del bloqueo de mutex, sino de la contención de mutex. Si estás usando locks de esta forma, deberías reevaluar tu código.
    Cada hilo bloquea y desbloquea el mutex cada vez que incrementa g_chores. Esto genera el overhead de adquirir y liberar el mutex con frecuencia, repetido 100,000 veces por hilo.
    Ese overhead oculta las diferencias reales de rendimiento entre los mecanismos de lock, porque el benchmark está dominado por la contención del lock y no por trabajo real. Este tipo de benchmark no sirve.

  • Soy fan de Justine y de su trabajo, pero probablemente este sea de los casos de prueba menos interesantes para un benchmark de mutex. Para empezar, debería evitarse una situación en la que varios hilos estén golpeando constantemente el mismo mutex.
    Por eso no me parece muy interesante qué implementación de mutex maneja mejor este caso.

    • Me pregunto qué considerarías un buen caso de prueba para hacer benchmark de mutex.
    • En la mayoría de los casos en que uso locks o semáforos, es alrededor de un recurso muy costoso. El uso de ese recurso domina por completo el overhead de rendimiento del lock.
    • Entonces, ¿qué habría que medir? El caso sin contención es importante y sirve como línea base, pero fuera de eso, el punto débil de un mutex es precisamente este. Si no maneja bien la contención, el hardware queda ocioso, aumenta el trabajo del scheduler o hay más entradas al kernel.
      Se omitió algo importante: bajo contención, un lock con mal rendimiento puede tener efectos sistémicos muy negativos, como crear hotspots en la red de memoria, y eso también se revelaría aquí.
    • No estoy del todo de acuerdo con la evaluación de que “varios hilos no deberían estar golpeando constantemente el mismo mutex”.
      Se me ocurren varios casos en los que varios hilos se concentran en el mismo mutex. Un ejemplo simple es llenar simultáneamente una estructura de datos como una lista o un diccionario.
      También podría hacerse con paso de mensajes, pero puede usar más memoria y ser más lento que esperar para escribir en una ubicación compartida.
  • Producción no se trata de velocidad, eficiencia ni de hacks obviamente “ingeniosos”.
    Si tengo que sacrificar el 50% de eficiencia para garantizar que no me llamen a las 3 de la mañana de un domingo a arreglar un sistema roto, elegiré eso cada vez.
    Producción se trata de confiabilidad, y escribir código confiable es 10 veces más difícil que escribir código “rápido”.