¿Cuál es el algoritmo más rápido para encontrar el número más grande en una matriz sin clasificar con múltiples procesadores?

Como Ricardo mencionó en su respuesta, para encontrar el número más alto real en una matriz, se deben verificar todos los elementos de la matriz. No hay forma de evitar eso. Esta encendido). Y tiene mucha razón sobre que es fácil de ejecutar en paralelo.

Sin embargo, si la matriz es lo suficientemente grande, supongamos que estamos tratando con una matriz infinita, es posible que verificar todos los elementos simplemente no sea factible, incluso con múltiples procesadores. En ese caso, puede emplear un método aleatorio. Verifique n elementos aleatorios en la matriz y use el más alto. Ya no se garantiza que encuentre el máximo real, pero puede estar lo suficientemente cerca como para que no importe. Aquí hay un ejemplo trivial, en Python. La primera función muestra cómo encontrar el máximo real, haciendo un bucle sobre todos los elementos de la matriz. El segundo muestra cómo encontrar algo lo suficientemente cerca mirando un subconjunto aleatorio de la matriz.

def max_correct (matriz):
# inicializa la variable max con el primer elemento
m = matriz [0]
para v en la matriz [1:]:
# para cada elemento en la matriz luego, verifique si
# ese valor es más alto que el máximo actual
m = v si v> m más m
volver m

def max_montecarlo (matriz, n):
tamaño = len (matriz)
n = min (n, tamaño)
# inicializa la variable max con un elemento aleatorio
m = matriz [random.randint (0, tamaño)]
para _ en rango (n):
# para un número dado de iteraciones, seleccione
# un elemento al azar y ver si es más grande
v = matriz [random.randint (0, tamaño)]
m = v si v> m más m
volver m

Este método generalmente no se usa para encontrar el máximo, sino el promedio. Se llama integración de Monte Carlo, y es excepcionalmente útil cuando se trata con tantos datos que simplemente no es factible verificarlos todos, o si tiene una función que no se puede integrar exactamente, y varias otras situaciones. Se utiliza en renderizado 3D no en tiempo real todo el tiempo. Un método comúnmente utilizado para la representación 3D es rastrear los diversos caminos que la luz puede atravesar una escena. Esto representa un conjunto infinito de intensidades de luz. No puede calcular todos los valores en una matriz infinita o recorrerlo para encontrar el promedio o el máximo, pero puede verificar un subconjunto finito elegido al azar, y si marca suficientes, estará lo suficientemente cerca como para que se produzca el error. ser irrelevante Esta es también la base para cosas como encuestas a clientes. Como no es factible preguntar a todos los clientes, simplemente elige un grupo de ellos de forma semialeatoria y extrapola. Mismo principio

Algunas notas

  • Los ejemplos de Python podrían hacerse mucho más compactos y rápidos mediante el uso de ciertas funciones en el módulo aleatorio . Lo escribí de esta manera para que sea más fácil de entender para los lectores sin conocimiento de Python.
  • Para matrices pequeñas y / o valores grandes para n , es más probable que un elemento se verifique más de una vez. Esto es fácil de optimizar. Puede usar un selector de números semialeatorio que está garantizado para no generar el mismo número más de una vez, por ejemplo, o mantener un registro de todos los elementos que ya están marcados, o construir el subarreglo en una estructura que no permita duplicados .
  • La calidad y fiabilidad de este tipo de enfoque depende en gran medida de la calidad de sus números aleatorios.
  • Ambos métodos son igualmente multiproceso.

editar: reparó un error en el código de ejemplo

Así que aquí hay una respuesta que no encontrará en CLRS:

Depende de tu sistema de archivos.

Digamos que tiene un sistema de clase Sequoia con ~ 93k nodos de cómputo y 1.5M núcleos. La DRAM total disponible es de 1.6 petabytes, así que supongamos que nuestra matriz es al menos tan grande y está ubicada en un disco en algún lugar esperando ser leída.

Ahora supongamos que hemos jodido y puesto la matriz en un sistema de archivos NFS que es efectivamente serial. La velocidad a la que podemos recorrer la matriz en DRAM es mucho, mucho más rápida que la velocidad a la que podemos extraer los datos del disco, por lo que no tiene ningún sentido usar más de un núcleo en un solo nodo.

Si utilizamos un sistema de archivos ridículamente sobreaprovisionado (y red) podemos tener un cabezal de lectura por nodo y cero contención de red. Si desea una respuesta exacta, es lo más rápido que puede obtenerla.

En una máquina más realista, tendrá una cantidad de flujos paralelos que es mucho, mucho menor que el número total de núcleos de procesador. Una vez que haya maximizado esas transmisiones, agregar más núcleos no hará que esto se ejecute más rápido.

Este problema es un buen ejemplo del problema del movimiento de datos. Se necesita mucho tiempo y energía para introducir bits en el núcleo del procesador, y si todo lo que vas a hacer hay una comparación y tal vez una escritura, tienes una máquina enormemente ineficiente.

La naturaleza del problema cambia por completo si la matriz se genera sobre la marcha y cada elemento de la matriz tarda varios milisegundos en generarse. Ahora puede poner en uso un millón de núcleos de procesador.

Debe revisar todos los números ya que la matriz no está ordenada (el número más grande podría estar en cualquier lugar , por lo que no puede omitir ningún número). Esto significa que el mejor algoritmo que puede esperar es un algoritmo O (n).

¿Existe tal algoritmo? Sí, y es bastante fácil: revisa los números uno a la vez, y para cada número verifica si es más grande que el más grande encontrado hasta ahora (que guardas en una variable). Si es así, asígnelo a la variable “más grande hasta ahora”. Cuando haya procesado todos los elementos, esa variable contiene el número más grande en la matriz.

Paralelizar este algoritmo es bastante simple: simplemente puede dividir la matriz en p partes, una de cada una será procesada por cada trabajo paralelo (por ejemplo, un hilo). Cada trabajo encuentra el número más grande en su sección de la matriz. Ahora tienes números p.

Finalmente, use el mismo método anterior para encontrar el número más grande entre esos números p (lo que probablemente pueda hacer eficientemente sin múltiples subprocesos, suponiendo que p no sea un número enorme).

Nota : la versión paralela no necesariamente se ejecutará más rápido que la versión en serie. Para un número muy pequeño de elementos, la sobrecarga de crear trabajos paralelos y hacer el paso final probablemente sea mucho más alta que el costo de simplemente ejecutar los elementos en un procesador.

La forma en que generalmente se trata es comparar la versión paralela con la versión en serie, para encontrar el punto en el que la versión paralela se vuelve más rápida. Luego, puede actualizar su implementación para hacer solo trabajo paralelo si el número de elementos a procesar es mayor que ese límite. Este límite probablemente depende del valor de p, así como de la máquina particular en la que ejecuta el código.

Gracias por el ata. Echa un vistazo al teorema de Brent – Everything2.com

Parece que quieres un máximo paralelo. Esta sería una instancia de una reducción paralela. Como la pregunta se planteó por solo max, comenzaré explicando qué es una reducción :

Explicaré qué es una reducción a través del lenguaje scala, aunque mi respuesta para una reducción paralela particular es con cuda en mente. Una reducción es una función de un contenedor con elementos de tipo T a T. Un ejemplo más concreto sería una función que toma una lista de enteros y devuelve su suma:

def sum (li: List [Int], acc: Int = 0): Int = li match {
caso h :: tl => sum (tl, pairWiseSum (acc, h))
caso _ => acc
}

Si esto no le resulta familiar, todo lo que dice es que si la lista está vacía, devuelva el acumulador; de lo contrario, agregue el primer elemento de la lista a la suma y agregue el resto de la lista de forma recursiva. Para encontrar el valor máximo podemos

def max (li: List [Int], acc: Int = Int.minimumValue): Int = li match {
caso h :: tl => max (tl, pairWiseMax (acc, h))
caso _ => acc
}

Entonces, se puede ver que la única diferencia entre max y sum es la inicialización de acc y el operador binario utilizado, pairWiseMax o pairWiseSum. Entonces podemos abstraer estas diferencias menores y obtener una clase completa de funciones de “reducción”:

def pairWiseSum (x: Int, y: Int): Int = x + y
def pairWiseMax (x: Int, y: Int): Int = if (x> y) x else y

Ahora una reducción paralela es donde queremos aprovechar el subprocesamiento múltiple para realizar una reducción. Un enfoque para la reducción paralela en una gpu que podría tener miles de núcleos sería un núcleo cuda (que se describe en un pseudocódigo) utilizando direccionamiento secuencial:

  1. divide tu matriz de entrada en bloques, por ejemplo, de k elementos que serán operados en paralelo por decir k hilos
  2. Luego, en cada bloque B de k elementos con k hilos, nos dividimos en dos partes. elementos de matriz a (i) con i = k / 2. Luego realizamos una serie telescópica de reducciones parciales actualizando la dirección indexada inferior:
  3. foreach a (j) con j
  4. a (j): = pairWiseMax (a (j), a (j + k / 2))
  5. }
  1. por ejemplo k: = 4, a: = {4,2,5,3} ahora hemos establecido
  2. a: = {max (4,5) = 5, max (2,3) = 3, 5, 3}
  • El siguiente paso es esperar primero a que los hilos se sincronicen y luego volver a la línea 3, pero esta vez con k: = k / 2. Entonces, si k fue 1024 inicial, dividiríamos la matriz en dos mitades (a1, a2) cada una de 512 elementos, realizaríamos la reducción parcial y luego descartaríamos a2. Ahora a1 es una matriz de 512 elementos, así que divídalo en dos mitades (b1, b2) de 256 y haga una reducción parcial y descarte b2 … etc … hasta que nos quedemos con un único elemento que será el máximo para este bloque.
  • Una vez que tenemos el máximo para cada bloque, podemos combinarlos, ya sea secuencialmente si el número de ellos es suficientemente pequeño o realizando más de estos pasos en un nuevo bloque formado a partir de los resultados de los bloques anteriores.
    1. Por ejemplo, si tuviéramos bloques {3,5}, {4,6} habríamos encontrado que el máximo en el primer bloque es 5, y el máximo en el segundo bloque es 6. Entonces podríamos formar un nuevo bloque {5 , 6} y ejecuta nuestra rutina una vez más.
    1. usando bloques de tamaño 2 aquí simplemente por claridad, el número no sería 2 para que esto tenga sentido, pero tal vez más como 2000

    Esta rutina se puede optimizar utilizando memoria compartida y mucho más, consulte http: //developer.download.nvidia … para obtener detalles y reducciones paralelas aún más rápidas.

    El problema a resolver no se limita solo a los microprocesadores, sino que también involucra múltiples núcleos en múltiples procesadores; para ese fin, me gustaría recomendar echar un vistazo a “clasificación de acceso alineado”: “Clasificación de acceso alineado (clasificación AA). La clasificación AA es adecuada para explotar tanto las instrucciones SIMD como el paralelismo a nivel de hilo disponible en la actualidad procesadores multinúcleo. La clasificación AA no implica ningún acceso a la memoria no alineado que atenúe el beneficio de las instrucciones SIMD, y por lo tanto ”

    http://researcher.watson.ibm.com

    El problema es O (n). El número de procesadores simplemente cambia la constante en su ecuación de complejidad. Si el tamaño de la matriz es relativamente pequeño, dividirlo en segmentos p y agilizar la ejecución a los procesadores p podría llevar más tiempo que simplemente realizar el mismo cálculo en menos de los procesadores p. El tiempo de ejecución real dependerá del tamaño de la matriz, el lugar donde se almacena (es decir, el tiempo de E / S) y la topología de memoria y comunicación de su sistema multiprocesador. Solo tenga en cuenta que no importa cuáles sean estas características, el problema es O (n).

    Respuesta corta: depende

    Respuesta más larga: probablemente lo más rápido sería subdividir el conjunto de entrada en p subconjuntos, encontrar el número más grande en cada subconjunto en un procesador dedicado y luego encontrar el mayor número de p resultantes. Pero si hay demasiados procesadores, no hay suficientes puntos de datos, el costo de paralelización es demasiado alto, entonces tal vez deberíamos usar solo una parte de nuestros procesadores p, posiblemente hasta uno.

    Puede ajustar el algoritmo para que se ajuste a cualquier computadora en la que se ejecute, pero probablemente no funcionaría tan bien para otras computadoras.