Supongamos que tengo una matriz de 1 000 000 de elementos y varios subprocesos de trabajo, cada uno de los cuales manipula datos en esta matriz. Los subprocesos de trabajo pueden estar actualizando elementos ya poblados con nuevos datos, pero cada operación está limitada a un solo elemento de matriz y es independiente de los valores de cualquier otro elemento.
El uso de un solo mutex para proteger toda la matriz claramente daría como resultado una gran contención. En el otro extremo, podría crear una matriz de exclusión mutua que tenga la misma longitud que la matriz original, y para cada array[i] bloquearía la mutex[i] mientras operaba en ella. Suponiendo una distribución uniforme de los datos, esto eliminaría principalmente la contención de bloqueo, a costa de una gran cantidad de memoria.
Creo que una solución más razonable sería tener una matriz de n mutexes (donde 1 < n < 1000000). Luego, para cada array[i] , bloquearía mutex[i % n] mientras operaba en él. Si n es lo suficientemente grande, aún puedo minimizar la contención.
Entonces, mi pregunta es, ¿existe una penalización de rendimiento al usar una gran cantidad (por ejemplo,> = 1000000) de mutexes de esta manera, más allá del aumento del uso de memoria? Si es así, ¿cuántos mutexes puede usar razonablemente antes de comenzar a ver la degradación?
Estoy seguro de que la respuesta a esto es algo específica de la plataforma; Estoy usando pthreads en Linux. También estoy trabajando en la configuración de mis propios puntos de referencia, pero la escala de datos en los que estoy trabajando hace que consuma mucho tiempo, por lo que agradecería alguna orientación inicial.
Esa fue la pregunta inicial. Para aquellos que solicitan información más detallada sobre el problema, tengo 4 archivos de datos binarios de varios GB que describen en algún lugar cerca de quinientos millones de eventos que se están analizando. La matriz en cuestión es en realidad la matriz de punteros que respaldan una tabla hash encadenada muy grande. Leemos los cuatro archivos de datos en la tabla hash, posiblemente agregándolos si comparten ciertas características. La implementación existente tiene 4 subprocesos, cada uno de los cuales lee un archivo e inserta registros de ese archivo en la tabla hash. La tabla hash tiene 997 bloqueos y 997*9973 = ~10 000 000 punteros. Al insertar un elemento con hash h , primero bloqueo mutex[h % 997] antes de insertar o modificar el elemento en bucket[h % 9943081] . Esto funciona bien y, por lo que sé, no hemos tenido demasiados problemas con la contención, pero hay un cuello de botella en el rendimiento porque solo usamos 4 núcleos de una máquina de 16 núcleos. (Y aún menos a medida que avanzamos, ya que los archivos generalmente no son todos del mismo tamaño). Una vez que todos los datos se han leído en la memoria, los analizamos, lo que utiliza nuevos hilos y una nueva estrategia de bloqueo ajustada a los diferentes carga de trabajo
Estoy intentando mejorar el rendimiento de la etapa de carga de datos cambiando a un grupo de subprocesos. En el nuevo modelo, todavía tengo un subproceso para cada archivo que simplemente lee el archivo en fragmentos de ~1 MB y pasa cada fragmento a un subproceso de trabajo en el grupo para analizar e insertar. La ganancia de rendimiento hasta ahora ha sido mínima, y el perfil que hice parecía indicar que el tiempo invertido en bloquear y desbloquear la matriz era el culpable probable. El bloqueo está integrado en la implementación de la tabla hash que estamos usando, pero permite especificar la cantidad de bloqueos que se usarán independientemente del tamaño de la tabla. Espero acelerar las cosas sin cambiar la implementación de la tabla hash.
(Una respuesta muy parcial y posiblemente indirecta a su pregunta).
Una vez obtuve un gran éxito de rendimiento al probar esto (en un CentOS) aumentando la cantidad de bloqueos de un máximo de ~ 1K a un máximo de ~ 1M. Si bien nunca entendí completamente su razón, finalmente descubrí (o simplemente me convencí) de que es la pregunta equivocada.
Suponga que tiene una matriz de longitud M , con n trabajadores. Además, utiliza una función hash para proteger los elementos M con bloqueos m < M (por ejemplo, mediante alguna agrupación aleatoria). Entonces, usando la aproximación al cuadrado de la paradoja del cumpleaños , la probabilidad de colisión entre dos trabajadores - p - viene dada por:
pag ~ norte 2 / (2m)
De ello se deduce que el número de mutexes que necesita, m , no depende de M en absoluto, es una función de p y n solamente.
Bajo Linux no hay otro costo que el de la memoria asociada con más mutexes.
Sin embargo , recuerde que la memoria utilizada por sus mutexes debe incluirse en su conjunto de trabajo, y si el tamaño de su conjunto de trabajo excede el tamaño de caché relevante, verá una caída significativa en el rendimiento. Esto significa que no desea una matriz mutex de tamaño excesivo.
Como señala Ami Tavory , la disputa depende de la cantidad de exclusiones mutuas y la cantidad de subprocesos, no de la cantidad de elementos de datos protegidos, por lo que no hay razón para vincular la cantidad de exclusiones mutuas a la cantidad de elementos de datos (con la condición obvia de que nunca tiene sentido tener más mutexes que elementos).
En el escenario general, aconsejaría
Simplemente bloqueando toda la matriz (simple, muy a menudo "lo suficientemente bueno" si su aplicación está haciendo principalmente "otras cosas" además de acceder a la matriz)
... o ...
Implementar un bloqueo de lectura/escritura en toda la matriz (suponiendo que las lecturas sean iguales o excedan las escrituras)
Aparentemente, su escenario no coincide con ninguno de los casos.
P: ¿Ha considerado implementar algún tipo de "cola de escritura"?
En el peor de los casos, solo necesitaría un mutex. En el mejor de los casos, incluso podría usar un mecanismo sin bloqueo para administrar su cola. Busque aquí algunas ideas que podrían ser aplicables: https://msdn.microsoft.com/en-us/library/windows/desktop/ee418650%28v=vs.85%29.aspx