Compartir a través de


Velocidad en PLINQ

En este artículo se proporciona información que le ayudará a escribir consultas PLINQ que sean lo más eficientes posible, a la vez que se producen resultados correctos.

El propósito principal de PLINQ es acelerar la ejecución de consultas LINQ to Objects mediante la ejecución de delegados de consulta en paralelo en equipos de varios núcleos. PLINQ funciona mejor cuando el procesamiento de cada elemento de una colección de origen es independiente, sin ningún estado compartido implicado entre los delegados individuales. Estas operaciones son comunes en LINQ to Objects y PLINQ y a menudo se denominan "perfectamente paralelas" porque se prestan fácilmente a la programación en varios subprocesos. Sin embargo, no todas las consultas consisten enteramente en operaciones maravillosamente paralelas. En la mayoría de los casos, una consulta implica algunos operadores que no se pueden paralelizar o que ralentizan la ejecución en paralelo. E incluso con consultas completamente paralelas, PLINQ todavía debe particionar el origen de datos y programar el trabajo en los subprocesos y normalmente combinar los resultados cuando se completa la consulta. Todas estas operaciones agregan al costo computacional de paralelización; estos costos de agregar paralelización se denominan sobrecarga. Para lograr un rendimiento óptimo en una consulta PLINQ, el objetivo es maximizar las partes que son deliciosamente paralelas y minimizar las piezas que requieren sobrecarga.

Factores que afectan al rendimiento de las consultas PLINQ

En las secciones siguientes se enumeran algunos de los factores más importantes que afectan al rendimiento de las consultas paralelas. Estas son instrucciones generales que por sí mismas no son suficientes para predecir el rendimiento de las consultas en todos los casos. Como siempre, es importante medir el rendimiento real de consultas específicas en equipos con una variedad de configuraciones y cargas representativas.

  1. Costo computacional del trabajo general.

    Para conseguir velocidad, una consulta PLINQ debe tener suficiente trabajo perfectamente paralelo para compensar la sobrecarga. El trabajo se puede expresar como el costo computacional de cada delegado multiplicado por el número de elementos de la colección de origen. Suponiendo que una operación se puede paralelizar, cuanto más costosa sea el cálculo, mayor será la oportunidad de acelerarse. Por ejemplo, si una función toma un milisegundo para ejecutarse, una consulta secuencial de más de 1000 elementos tardará un segundo en realizar esa operación, mientras que una consulta paralela en un equipo con cuatro núcleos podría tardar solo 250 milisegundos. Esto da como resultado una velocidad de 750 milisegundos. Si la función requería un segundo para ejecutarse para cada elemento, la velocidad sería de 750 segundos. Si el delegado es muy caro, PLINQ podría proporcionar un aumento significativo de la velocidad con solo unos pocos elementos de la colección de origen. Por el contrario, colecciones de fuentes pequeñas con delegados triviales no son usualmente buenos candidatos para PLINQ.

    En el ejemplo siguiente, queryA probablemente es un buen candidato para PLINQ, suponiendo que su función Select implique mucho trabajo. queryB probablemente no es una buena candidata porque no hay suficiente trabajo en la instrucción Select, y la sobrecarga de la paralelización compensará la mayoría o la totalidad de la velocidad.

    Dim queryA = From num In numberList.AsParallel()  
                 Select ExpensiveFunction(num); 'good for PLINQ  
    
    Dim queryB = From num In numberList.AsParallel()  
                 Where num Mod 2 > 0  
                 Select num; 'not as good for PLINQ  
    
    var queryA = from num in numberList.AsParallel()  
                 select ExpensiveFunction(num); //good for PLINQ  
    
    var queryB = from num in numberList.AsParallel()  
                 where num % 2 > 0  
                 select num; //not as good for PLINQ  
    
  2. Número de núcleos lógicos en el sistema (grado de paralelismo).

    Este punto es un corolario obvio de la sección anterior, las consultas que son deliciosamente paralelas se ejecutan más rápido en las máquinas con más núcleos, ya que el trabajo se puede dividir entre más subprocesos simultáneos. La cantidad total de velocidad depende de qué porcentaje del trabajo total de la consulta se puede paralelizar. Sin embargo, no suponga que todas las consultas se ejecutarán dos veces más rápido en un equipo de ocho núcleos como un equipo de cuatro núcleos. Al optimizar las consultas para obtener un rendimiento óptimo, es importante medir los resultados reales en equipos con varios números de núcleos. Este punto está relacionado con el punto 1: se requieren conjuntos de datos más grandes para aprovechar mayores recursos informáticos.

  3. Número y tipo de operaciones.

    PLINQ proporciona el operador AsOrdered para situaciones en las que es necesario mantener el orden de los elementos en la secuencia de origen. Hay un costo asociado a la ordenación, pero este costo suele ser modesto. Las operaciones GroupBy y Join también incurren en sobrecarga. PLINQ funciona mejor cuando se permite procesar elementos de la colección de origen en cualquier orden y pasarlos al siguiente operador tan pronto como estén listos. Para obtener más información, vea Conservación de pedidos en PLINQ.

  4. La forma de ejecución de consultas.

    Si va a almacenar los resultados de una consulta llamando a ToArray o ToList, los resultados de todos los subprocesos paralelos deben combinarse en la estructura de datos única. Esto implica un costo computacional inevitable. Asimismo, si se recorren en iteración los resultados mediante el uso de un bucle foreach (For Each en Visual Basic), los resultados de los subprocesos de trabajo deben serializarse en el subproceso del enumerador. Pero si solo desea realizar alguna acción en función del resultado de cada subproceso, puede usar el método ForAll para realizar este trabajo en varios subprocesos.

  5. Tipo de opciones de combinación.

    PLINQ se puede configurar para almacenar en búfer su salida y generarlo en fragmentos o en todos a la vez después de que se genere todo el conjunto de resultados, o bien para transmitir resultados individuales a medida que se generan. El primero da como resultado una disminución del tiempo de ejecución general y el segundo da como resultado una disminución de la latencia entre los elementos devueltos. Aunque las opciones de combinación no siempre tienen un impacto importante en el rendimiento general de las consultas, pueden afectar al rendimiento percibido porque controlan cuánto tiempo debe esperar un usuario para ver los resultados. Para obtener más información, vea Opciones de combinación en PLINQ.

  6. El tipo de partición.

    En algunos casos, una consulta PLINQ sobre una colección de origen indexable puede dar lugar a una carga de trabajo desequilibrada. Cuando esto ocurre, es posible que pueda aumentar el rendimiento de las consultas mediante la creación de un particionador personalizado. Para obtener más información, consulte Particionadores personalizados para PLINQ y TPL.

Cuando PLINQ elige el modo secuencial

PLINQ siempre intentará ejecutar una consulta al menos tan rápido como la consulta se ejecutaría secuencialmente. Aunque PLINQ no examina el costo computacional de los delegados de usuario o el tamaño del origen de entrada, busca ciertas "formas" de consulta. En concreto, busca operadores de consulta o combinaciones de operadores que normalmente hacen que una consulta se ejecute más lentamente en modo paralelo. Cuando encuentra estas formas, PLINQ de forma predeterminada vuelve al modo secuencial.

Sin embargo, después de medir el rendimiento de una consulta específica, puede determinar que realmente se ejecuta más rápido en modo paralelo. En tales casos, puede usar el ParallelExecutionMode.ForceParallelism indicador mediante el WithExecutionMode método para indicar a PLINQ que paralelice la consulta. Para obtener más información, vea Cómo: Especificar el modo de ejecución en PLINQ.

En la lista siguiente se describen las formas de consulta que PLINQ ejecutará de forma predeterminada en modo secuencial:

  • Las consultas que contienen una instrucción Select, Where indexada, SelectMany indexada o una cláusula ElementAt después de un operador de ordenación o filtrado que ha quitado o reorganizado los índices originales.

  • Consultas que contienen un operador Take, TakeWhile, Skip, SkipWhile, en las que los índices de la secuencia de origen no están en el orden original.

  • Las consultas que contienen Zip o SequenceEquals, a menos que uno de los orígenes de datos tenga un índice ordenado originalmente y el otro origen de datos sea indexable (es decir, una matriz o IList(T)).

  • Consultas que contienen Concat, a menos que se aplique a orígenes de datos indexables.

  • Consultas que contienen Reverse, a menos que se apliquen a un origen de datos indexable.

Consulte también