Diferencia entre las políticas de ejecución y cuándo usarlas

Diferencia entre las políticas de ejecución y cuándo usarlas

¿Cuál es la diferencia entre seq y par /par_unseq ?

std::for_each(std::execution::seq, std::begin(v), std::end(v), function_call);

std::execution::seq significa ejecución secuencial. Es el valor predeterminado si no especifica la política de ejecución en absoluto. Obligará a la implementación a ejecutar todas las llamadas a funciones en secuencia. También se garantiza que todo sea ejecutado por el subproceso de llamada.

Por el contrario, std::execution::par y std::execution::par_unseq implica ejecución paralela. Eso significa que promete que todas las invocaciones de la función dada se pueden ejecutar de forma segura en paralelo sin violar ninguna dependencia de datos. La implementación puede usar una implementación paralela, aunque no está obligada a hacerlo.

¿Cuál es la diferencia entre par y par_unseq ?

par_unseq requiere garantías más fuertes que par , pero permite optimizaciones adicionales. Específicamente, par_unseq requiere la opción de intercalar la ejecución de múltiples llamadas a funciones en el mismo hilo.

Ilustremos la diferencia con un ejemplo. Suponga que desea paralelizar este bucle:

std::vector<int> v = { 1, 2, 3 };
int sum = 0;
std::for_each(std::execution::seq, std::begin(v), std::end(v), [&](int i) {
  sum += i*i;
});

No puede paralelizar directamente el código anterior, ya que introduciría una dependencia de datos para el sum variable. Para evitar eso, puedes introducir un candado:

int sum = 0;
std::mutex m;
std::for_each(std::execution::par, std::begin(v), std::end(v), [&](int i) {
  std::lock_guard<std::mutex> lock{m};
  sum += i*i;
});

Ahora todas las llamadas a funciones se pueden ejecutar de forma segura en paralelo, y el código no se romperá cuando cambie a par . Pero, ¿qué pasaría si usas par_unseq? en cambio, ¿dónde un subproceso podría ejecutar múltiples llamadas a funciones no en secuencia sino al mismo tiempo?

Puede resultar en un interbloqueo, por ejemplo, si el código se reordena así:

 m.lock();    // iteration 1 (constructor of std::lock_guard)
 m.lock();    // iteration 2
 sum += ...;  // iteration 1
 sum += ...;  // iteration 2
 m.unlock();  // iteration 1 (destructor of std::lock_guard)
 m.unlock();  // iteration 2

En el estándar, el término es vectorización-insegura . Para citar de P0024R2:

Una forma de hacer que el código anterior sea seguro para la vectorización es reemplazar el mutex por un atómico:

std::atomic<int> sum{0};
std::for_each(std::execution::par_unseq, std::begin(v), std::end(v), [&](int i) {
  sum.fetch_add(i*i, std::memory_order_relaxed);
});

¿Cuáles son las ventajas de usar par_unseq? sobre par ?

Las optimizaciones adicionales que una implementación puede usar en par_unseq El modo incluye ejecución vectorizada y migraciones de trabajo entre subprocesos (este último es relevante si se usa el paralelismo de tareas con un programador de robo de elementos primarios).

Si se permite la vectorización, las implementaciones pueden usar internamente el paralelismo SIMD (instrucción única, datos múltiples). Por ejemplo, OpenMP lo admite a través de #pragma omp simd anotaciones, que pueden ayudar a los compiladores a generar mejor código.

¿Cuándo debo preferir std::execution::seq? ?

  1. corrección (evitando carreras de datos)
  2. evitar la sobrecarga paralela (costos de inicio y sincronización)
  3. simplicidad (depuración)

No es raro que las dependencias de datos impongan la ejecución secuencial. En otras palabras, use la ejecución secuencial si la ejecución en paralelo agregaría carreras de datos.

Reescribir y ajustar el código para la ejecución en paralelo no siempre es trivial. A menos que sea una parte crítica de su aplicación, puede comenzar con una versión secuencial y optimizarla más tarde. También es posible que desee evitar la ejecución en paralelo si está ejecutando el código en un entorno compartido en el que debe ser conservador en el uso de recursos.

El paralelismo tampoco es gratis. Si el tiempo de ejecución total esperado del ciclo es muy bajo, la ejecución secuencial probablemente será la mejor, incluso desde una perspectiva pura de rendimiento. Cuanto más grandes sean los datos y más costoso sea cada paso de cálculo, menos importante será la sobrecarga de sincronización.

Por ejemplo, usar el paralelismo en el ejemplo anterior no tendría sentido, ya que el vector solo contiene tres elementos y las operaciones son muy económicas. También tenga en cuenta que la versión original, antes de la introducción de mutexes o atomics, no contenía sobrecarga de sincronización. Un error común al medir la velocidad de un algoritmo paralelo es usar una versión paralela que se ejecuta en una CPU como línea de base. En su lugar, siempre debe comparar con una implementación secuencial optimizada sin la sobrecarga de sincronización.

¿Cuándo debo preferir std::execution::par_unseq ?

Primero, asegúrese de que no sacrifique la corrección:

  • Si hay carreras de datos al ejecutar pasos en paralelo por diferentes subprocesos, par_unseq no es una opción.
  • Si el código es no seguro para la vectorización , por ejemplo, porque adquiere un bloqueo, par_unseq no es una opción (pero par podría ser).

De lo contrario, use par_unseq si es una pieza crítica para el rendimiento y par_unseq mejora el rendimiento sobre seq .

¿Cuándo debo preferir std::execution::par? ?

Si los pasos se pueden ejecutar de forma segura en paralelo, pero no puede usar par_unseq porque es no segura para la vectorización , es candidato a par .

Me gusta seq_unseq , verifique que sea una pieza crítica para el rendimiento y par es una mejora de rendimiento sobre seq .

Fuentes:

  • cppreference.com (política de ejecución)
  • P0024R2:El TS de paralelismo debe ser estandarizado

seq significa "ejecutar secuencialmente" y es exactamente lo mismo que la versión sin política de ejecución.

par significa "ejecutar en paralelo", lo que permite que la implementación se ejecute en varios subprocesos en paralelo. Usted es responsable de asegurarse de que no ocurran carreras de datos dentro de f .

par_unseq significa que además de poder ejecutarse en varios subprocesos, la implementación también puede intercalar iteraciones de bucle individuales dentro de un único subproceso, es decir, cargar varios elementos y ejecutar f en todos ellos sólo después. Esto es necesario para permitir una implementación vectorizada.