2010-02-06 609 views
5

Si quiero paralelizar la ejecución de un algoritmo, ¿cuáles son los pequeños trozos de código que debo dividir?¿Qué cantidad de ejecución de código debo paralelizar?

Un ejemplo clásico es un algoritmo de clasificación. ¿Para qué tamaño de elemento o tiempo de ejecución típico tiene sentido dividir la clasificación entre múltiples hilos? ¿O cuándo es la sobrecarga para esperar en otro subproceso más grande que el tiempo de ejecución en un solo subproceso?

¿Hay reglas simples? ¿Esto depende del sistema operativo?

+2

pregunta difícil de responder: realmente depende del dominio del problema y las restricciones de hardware, es decir, la cantidad de elementos que está ordenando y en qué hardware – zebrabox

Respuesta

4

La regla clave es "horquilla solo cuando la sobrecarga de bifurcación es mucho menor que la cantidad de trabajo que hará la horquilla". Debido a que la bifurcación por sobrecarga es una propiedad de la tecnología específica que utiliza, y también lo es el esfuerzo para hacer el trabajo, usted en cierto sentido tiene que determinar esto empíricamente. Es probable que termine con una constante de ajuste de umbral en su código para representar esta compensación.

Lo que descubrirá en la práctica es que encontrar trozos de trabajo realmente difíciles. Si reduce el tamaño del trabajo, no tiene muchas dependencias y puede programarlo una vez que todos sus flujos de datos de entrada estén listos. Pero pequeños trozos generalmente significan trabajo pequeño, y la bifurcación por encima generalmente niega la ganancia. Si intentas hacer grandes los trozos, tienen tantas dependencias que no puedes separarlas para programarlas.

Algunas personas tienen suerte y pueden encontrar trozos tan grandes; llamamos a la mayoría de esas personas físicos y/o programadores de Fortran y están aprovechando el paralelismo de datos inducido al dividir el mundo en tantas piezas pequeñas como les sea posible.

La única cura decente que conozco es usar un mecanismo de horquilla espectacularmente rápido, para que pueda encontrar los trozos más pequeños y prácticos. Desafortunadamente, las bibliotecas de paralelismo que se ofrecen para hacer esto son ... bibliotecas, invocadas dinámicamente, con la sobrecarga de invocación dinámica correspondiente. Las bibliotecas típicas que contienen primitivas de paralelismo demoran entre 100 y miles de ciclos para implementar un "tenedor"; estas son malas noticias si tu pedazo de trabajo son 100 instrucciones de la máquina.

Creo firmemente que para obtener mecanismos de bifurcación tan rápidos, el compilador del lenguaje debe saber que está haciendo la bifurcación, por ejemplo, "bifurcación" (aunque deletreado :-) sea una palabra clave en el idioma. Luego el compilador puede ver las horquillas y preasignar todo lo necesario para minimizar el tiempo para lograr esto, y generar código especial para administrar los pasos de bifurcación (y unión).

El lenguaje PARLANSE que diseñé, y que usamos en Semantic Designs es uno de esos idiomas. Es un lenguaje parecido a Lisp en sintaxis (pero no en semántica). Su operador de paralelismo se deletrea "(|| ...)". Puede verlo a continuación en el módulo de Quicksort que utilizamos a diario, a continuación. También puede ver el valor explícito QuickSortParallelThreshold, determinado empíricamente. Este Quicksort escala linealmente a 8 núcleos en un sistema Intel x86.

(define QuickSort 
    (module 
    (;; (define Value nu) 
     (compileifthen (~ (defined QuickSortWithParlanseBuiltInOrderingOfNu)) 
      (define QuickSortWithParlanseBuiltInOrderingOfNu ~f) ; use PARLANSE comparison operators 
     )compileifthen 
     (compileifthen (~ (defined QuickSortParallelThreshold)) 
      (define QuickSortParallelThreshold 100) 
     )compileifthen 
     (compileifthen (~ (defined QuickSortThreshold)) 
      (compileifthenelse QuickSortWithParlanseBuiltInOrderingOfNu 
      (define QuickSortThreshold 16) 
      (define QuickSortThreshold 8) 
     )compileifthenelse 
     )compileifthen 
     (compileifthenelse (~ (defined QuickSortWithCompareByReference)) 
      (define QuickSortWithCompareByReference ~f) 
      (compileifthen QuickSortWithParlanseBuiltInOrderingOfNu 
      (define QuickSortWithCompareByReference ~f) 
     )compileifthen 
     )compileifthenelse 
     (define SortRange 
      (action (procedure (structure (compileifthen (~ QuickSortWithParlanseBuiltInOrderingOfNu) 
              (compileifthenelse (~ QuickSortWithCompareByReference) 
              [compare (function (sort integer (range -1 +1)) (structure [value1 Value] [value2 Value]))] 
              [compare (function (sort integer (range -1 +1)) (structure [value1 (reference Value)] [value2 (reference Value)]))] 
             )compileifthenelse 
             )compileifthen 
             [a (reference (array Value 1 dynamic))] 
             [from natural] 
             [to natural] 
          )structure 
       )procedure 
      (local (;; (define quicksort 
         (action (procedure (structure [l integer] [r integer]))) 
         )define 

         (define quicksort 
         (action (procedure (structure [l integer] [r integer])) 
          (ifthenelse (<= (- r l) (coerce integer QuickSortThreshold)) 
          (do [i integer] (++ l) r +1 
           (local (= [exch Value] a:i) 
           (block exit_if_inserted 
            (;; (do [j integer] (-- i) l -1 
             (ifthenelse (compileifthenelse QuickSortWithParlanseBuiltInOrderingOfNu 
                 (> a:j exch) 
                 (compileifthenelse (~ QuickSortWithCompareByReference) 
                 (== (compare a:j exch) +1) 
                 (== (compare (. a:j) (. exch)) +1) 
                 )compileifthenelse 
                )compileifthenelse 
              (= a:(++ j) a:j) 
              (;; (= a:(++ j) exch) 
               (exitblock exit_if_inserted) 
              );; 
             )ifthenelse 
             )do 
             (= a:l exch) 
            );; 
           )block 
           )local 
          )do 
          (local (;; (= [i integer] l) 
             (= [j integer] r) 
             (= [p integer] l) 
             (= [q integer] r) 
             [exch Value] 
            );; 
           (;; 
            `use middle element as pivot': 
            (local (= [m integer] (// (+ l r) +2)) 
             (;; (= exch a:m) 
              (= a:m a:r) 
              (= a:r exch) 
            );; 
            )local 
            `4-way partitioning = < > =': 
            (loop exit_if_partitioned 
             (;; 
             `find element greater than pivot': 
              (loop exit_if_greater_than_found 
              (;; (compileifthenelse QuickSortWithParlanseBuiltInOrderingOfNu 
                (ifthenelse (< a:i a:r) 
                (consume ~t) 
                (ifthenelse (> a:i a:r) 
                 (exitblock exit_if_greater_than_found) 
                 (;; (ifthen (>= i j) 
                  (exitblock exit_if_partitioned) 
                  )ifthen 
                  (= exch a:p) 
                  (= a:p a:i) 
                  (= a:i exch) 
                  (+= p 1) 
                 );; 
                )ifthenelse 
                )ifthenelse 
                (case (compileifthenelse (~ QuickSortWithCompareByReference) 
                  (compare a:i a:r) 
                  (compare (. a:i) (. a:r)) 
                 )compileifthenelse 
                -1 
                 (consume ~t) 
                +1 
                 (exitblock exit_if_greater_than_found) 
                else (;; (ifthen (>= i j) 
                   (exitblock exit_if_partitioned) 
                  )ifthen 
                   (= exch a:p) 
                   (= a:p a:i) 
                   (= a:i exch) 
                   (+= p 1) 
                 );; 
                )case 
               )compileifthenelse 
               (+= i 1) 
              );; 
              )loop 
             `find element less than to pivot': 
              (loop exit_if_less_than_found 
              (;; (-= j 1) 
               (ifthen (>= i j) 
                (exitblock exit_if_partitioned) 
               )ifthen 
               (compileifthenelse QuickSortWithParlanseBuiltInOrderingOfNu 
                (ifthenelse (< a:j a:r) 
                (exitblock exit_if_less_than_found) 
                (ifthenelse (> a:j a:r) 
                 (consume ~t) 
                 (;; (-= q 1) 
                  (= exch a:j) 
                  (= a:j a:q) 
                  (= a:q exch) 
                 );; 
                )ifthenelse 
                )ifthenelse 
                (case (compileifthenelse (~ QuickSortWithCompareByReference) 
                  (compare a:j a:r) 
                  (compare (. a:j) (. a:r)) 
                 )compileifthenelse 
                -1 
                 (exitblock exit_if_less_than_found) 
                +1 
                 (consume ~t) 
                else (;; (-= q 1) 
                   (= exch a:j) 
                   (= a:j a:q) 
                   (= a:q exch) 
                 );; 
                )case 
               )compileifthenelse 
              );; 
              )loop 
             `move found elements to proper partitions': 
              (;; (= exch a:i) 
               (= a:i a:j) 
               (= a:j exch) 
              );; 
             `increment index': 
              (+= i 1) 
            );; 
            )loop 
            `3-way partitioning <=>': 
            (;; 
             `move pivot to final location': 
             (;; (= exch a:i) 
              (= a:i a:r) 
              (= a:r exch) 
              (= j (-- i)) 
              (= i (++ i)) 
             );; 
             `move elements equal to pivot to final locations': 
             (;; (do [k integer] l (-- p) +1 
               (;; (= exch a:k) 
                (= a:k a:j) 
                (= a:j exch) 
                (-= j 1) 
               );; 
              )do 
              (do [k integer] (-- r) q -1 
               (;; (= exch a:i) 
                (= a:i a:k) 
                (= a:k exch) 
                (+= i 1) 
               );; 
              )do 
             );; 
            );; 
            `sort partitions not equal to pivot': 
            (ifthenelse (<= (- r l) (coerce integer QuickSortParallelThreshold)) 
             (;; (quicksort l j) 
              (quicksort i r) 
            );; 
             (|| (quicksort l j) 
              (quicksort i r) 
            )|| 
            )ifthenelse 
           );; 
          )local 
          )ifthenelse 
         )action 
         )define 

        );; 
       (;; (quicksort (coerce integer from) (coerce integer to)) 
        (ifdebug (do [i integer] (coerce integer from) (-- (coerce integer to)) +1 
          (trust (compileifthenelse QuickSortWithParlanseBuiltInOrderingOfNu 
             (<= a:i a:(++ i)) 
             (compileifthenelse (~ QuickSortWithCompareByReference) 
             (<= (compare a:i a:(++ i)) +0) 
             (<= (compare (. a:i) (. a:(++ i))) +0) 
            )compileifthenelse 
            )compileifthenelse 
            `QuickSort:Sort -> The array is not sorted.' 
          )trust 
          )do 
       )ifdebug 
      );; 
      )local 
     )action 
     )define 

     (define Sort 
      (action (procedure (structure (compileifthen (~ QuickSortWithParlanseBuiltInOrderingOfNu) 
              (compileifthenelse (~ QuickSortWithCompareByReference) 
              [compare (function (sort integer (range -1 +1)) (structure [value1 Value] [value2 Value]))] 
              [compare (function (sort integer (range -1 +1)) (structure [value1 (reference Value)] [value2 (reference Value)]))] 
             )compileifthenelse 
             )compileifthen 
             [a (reference (array Value 1 dynamic))] 
          )structure 
       )procedure 
      (compileifthenelse (~ QuickSortWithParlanseBuiltInOrderingOfNu) 
       (SortRange compare a (coerce natural (lowerbound (@ a) 1)) (coerce natural (upperbound (@ a) 1))) 
       (SortRange a (coerce natural (lowerbound (@ a) 1)) (coerce natural (upperbound (@ a) 1))) 
      )compileifthenelse 
     )action 
     )define 

    );; 
)module 
)define 
2

Depende de la sobrecarga de la comunicación entre hilos. Probé openMP con procesamiento de imágenes, y allí una línea de píxeles era conveniente, y daba buenas velocidades. Mi imagen era una megapíxel, por lo que hubo 1000 tareas, lo que es probablemente más que suficiente para mantener ocupadas las máquinas de muchos puntajes actuales. Tampoco necesita limitarse a trabajos que tarden más de un segundo más o menos. En este ejemplo, las aceleraciones de trabajos del orden de 10 milisegundos fueron claramente visibles.

Ahora bien, este fue un algoritmo agradable porque no era recursivo, por lo que no había dependencias de una tarea en la otra, y todas las tareas eran automáticamente del mismo tamaño.

Los algoritmos de ordenamiento serán más difíciles debido a los diferentes tamaños de tarea. Desea poder experimentar con esto, y tal vez elegir un tipo que sea más fácil de paralelizar.

1

Tome un par de cursos en programación simultánea y paralela. Aprende varias tecnologías, como la antigua horquilla simple & forget o multihilo "manual" (subprocesos Java o pthreads), MPI, OpenMP, BSP, incluso CUDA o OpenCL. Luego decida ser un experto o deje que los expertos diseñen e implementen algoritmos paralelos eficientes y correctos. La parte "paralela" es fácil, las partes "eficiente" y "correcta" no lo son, cuando ambas son necesarias. Incluso la colección Vector concurrente de Java, diseñada e implementada por expertos, no estaba libre de errores en las primeras versiones. ¡La mera definición de modelo de memoria no estaba clara en las primeras versiones del estándar Java!

La regla más simple: uso de componentes listos para su uso diseñado e implementado por expertos y no trate de lograr tanto la exactitud y la eficiencia de diseñar sus propios algoritmos paralelos a menos que seas un experto.

1

Resolver este problema programáticamente es uno de los santos grises de la computación paralela, y hay muchas bibliotecas que pueden aproximar el paralelismo óptimo para problemas particulares (por ejemplo, Data Parallel Haskell).

De todos modos, al hacer esto a mano, es necesario comprender:

  • El algoritmo que desea poner en paralelo (? ¿Es paralelizable)
  • Las características de los datos, por ejemplo, el tamaño, la ubicación (en el disco, en la memoria), etc.
  • El hardware que está ejecutando, por ejemplo, núcleos numéricos, latencia de memoria, tamaños de caché/líneas/asociación, etc.
  • El modelo de subprocesamiento del lenguaje de implementación (corutinas, hilos verdes, hilos del sistema operativo) y sistema operativo.
  • Costo de desove y cambio de contexto entre subprocesos.

Suponiendo que el algoritmo es paralelizable, su objetivo es encontrar el número de subprocesos y el tamaño de fragmento relativo de los datos, de modo que pueda hacer un uso óptimo del hardware para generar una solución.

Esto es bastante difícil de hacer sin mucha experimentación. Mi forma preferida de calcular esto es mediante la ejecución de un montón de puntos de referencia, y obtener datos de rendimiento en función de una o más combinaciones de lo siguiente:

  • Número de hilos.
  • tamaños de búfer (si los datos no está en RAM) incrementando en algún valor razonable (por ejemplo, tamaño de bloque, tamaño de paquete, el tamaño de caché, etc.)
  • Variando tamaños chunk (si se puede procesar los datos de forma incremental).
  • Diversas perillas de sintonización para el sistema operativo o el tiempo de ejecución del idioma.
  • Fijar los hilos a las CPU para mejorar la localidad.
  • Etc.

De todos modos, esto no es tarea fácil, y existen herramientas y bibliotecas para ayudar a apretar tanto el rendimiento como es posible salir de sus problemas paralelizables.La única forma razonable de hacerlo correctamente es tener una buena comprensión de sus datos, su código y su entorno de tiempo de ejecución.

Cuestiones relacionadas