Warning: file_get_contents(/data/phpspider/zhask/data//catemap/1/cocoa/3.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
C# 使用并行FOR循环节省时间_C#_.net_Parallel Extensions - Fatal编程技术网

C# 使用并行FOR循环节省时间

C# 使用并行FOR循环节省时间,c#,.net,parallel-extensions,C#,.net,Parallel Extensions,我有一个关于循环并行的问题。我有以下代码: public static void MultiplicateArray(double[] array, double factor) { for (int i = 0; i < array.Length; i++) { array[i] = array[i] * factor; } } public static void Multipli

我有一个关于循环并行的问题。我有以下代码:

    public static void MultiplicateArray(double[] array, double factor)
    {
        for (int i = 0; i < array.Length; i++)
        {
            array[i] = array[i] * factor;
        }
    }

    public static void MultiplicateArray(double[] arrayToChange, double[] multiplication)
    {
        for (int i = 0; i < arrayToChange.Length; i++)
        {
            arrayToChange[i] = arrayToChange[i] * multiplication[i];
        }
    }

    public static void MultiplicateArray(double[] arrayToChange, double[,] multiArray, int dimension)
    {
        for (int i = 0; i < arrayToChange.Length; i++)
        {
            arrayToChange[i] = arrayToChange[i] * multiArray[i, dimension];
        }
    }

问题是,我想节省时间,而不是浪费时间。标准for循环的计算时间约为2分钟,而并行for循环的计算时间为3分钟。为什么?

parallel.for涉及更复杂的内存管理。这一结果可能因cpu规格而异,如#内核、一级和二级缓存

请看这篇有趣的文章:

来自和

您没有创建执行for的三个线程/进程,但是for的迭代尝试并行执行,因此即使只有一个for,您也使用多个线程/进程

这意味着可以同时执行索引为0和索引为1的交互

很可能您正在强制使用太多的线程/进程,并且创建/执行这些线程/进程的开销大于速度增益

尝试在三个不同的线程/进程中使用三个普通for,如果您的系统是多核的(至少是3倍),那么并行所需时间应该不到一分钟。
for()
可以通过并行化代码大大提高性能,但它也有开销(线程之间的同步,在每次迭代时调用委托)。由于在您的代码中,每次迭代都非常短(基本上,只有几个CPU指令),因此这种开销可能会变得非常显著

因此,我认为使用
Parallel.For()
不是适合您的解决方案。相反,如果您手动并行化代码(在本例中非常简单),您可能会看到性能的提高

为了验证这一点,我进行了一些测量:我在一个包含2000000项的数组上运行了不同的
MultiplicateArray()
(我使用的代码如下)。在我的机器上,串行版本始终需要0.21秒和
并行。For()
通常需要0.45秒左右,但有时会达到8-9秒

首先,我将尝试改进常见情况,稍后我将讨论这些问题。我们希望通过N个CPU处理阵列,因此我们将其分成N个大小相等的部分,并分别处理每个部分。结果如何?0.35秒。这比连载版本还差。但是
for
在数组中的每个项上循环是最优化的结构之一。我们不能做点什么来帮助编译器吗?提取循环的边界可能会有所帮助。结果是:0.18秒。这比串行版本好,但也不多。而且,有趣的是,在我的4核机器上,将并行度从4更改为2(没有超线程)并不会改变结果:仍然是0.18秒。这让我得出结论,CPU不是这里的瓶颈,内存带宽是

现在,回到峰值:我的自定义并行化没有它们,但是
Parallel.For()
有,为什么
Parallel.For()
确实使用范围分区,这意味着每个线程处理数组中自己的部分。但是,如果一个线程提前完成,它将尝试帮助处理另一个尚未完成的线程的范围。如果发生这种情况,您将获得大量错误共享,这可能会大大降低代码的速度。我自己对强迫虚假分享的测试似乎表明这确实可能是问题所在。强制执行
Parallel.For()
的并行度似乎有助于缓解峰值

当然,所有这些测量都是特定于我的计算机上的硬件的,对您来说会有所不同,因此您应该自己进行测量

我使用的代码是:

static void Main()
{
    double[] array = new double[200 * 1000 * 1000];

    for (int i = 0; i < array.Length; i++)
        array[i] = 1;

    for (int i = 0; i < 5; i++)
    {
        Stopwatch sw = Stopwatch.StartNew();
        Serial(array, 2);
        Console.WriteLine("Serial: {0:f2} s", sw.Elapsed.TotalSeconds);

        sw = Stopwatch.StartNew();
        ParallelFor(array, 2);
        Console.WriteLine("Parallel.For: {0:f2} s", sw.Elapsed.TotalSeconds);

        sw = Stopwatch.StartNew();
        ParallelForDegreeOfParallelism(array, 2);
        Console.WriteLine("Parallel.For (degree of parallelism): {0:f2} s", sw.Elapsed.TotalSeconds);

        sw = Stopwatch.StartNew();
        CustomParallel(array, 2);
        Console.WriteLine("Custom parallel: {0:f2} s", sw.Elapsed.TotalSeconds);

        sw = Stopwatch.StartNew();
        CustomParallelExtractedMax(array, 2);
        Console.WriteLine("Custom parallel (extracted max): {0:f2} s", sw.Elapsed.TotalSeconds);

        sw = Stopwatch.StartNew();
        CustomParallelExtractedMaxHalfParallelism(array, 2);
        Console.WriteLine("Custom parallel (extracted max, half parallelism): {0:f2} s", sw.Elapsed.TotalSeconds);

        sw = Stopwatch.StartNew();
        CustomParallelFalseSharing(array, 2);
        Console.WriteLine("Custom parallel (false sharing): {0:f2} s", sw.Elapsed.TotalSeconds);
    }
}

static void Serial(double[] array, double factor)
{
    for (int i = 0; i < array.Length; i++)
    {
        array[i] = array[i] * factor;
    }
}

static void ParallelFor(double[] array, double factor)
{
    Parallel.For(
        0, array.Length, i => { array[i] = array[i] * factor; });
}

static void ParallelForDegreeOfParallelism(double[] array, double factor)
{
    Parallel.For(
        0, array.Length, new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount },
        i => { array[i] = array[i] * factor; });
}

static void CustomParallel(double[] array, double factor)
{
    var degreeOfParallelism = Environment.ProcessorCount;

    var tasks = new Task[degreeOfParallelism];

    for (int taskNumber = 0; taskNumber < degreeOfParallelism; taskNumber++)
    {
        // capturing taskNumber in lambda wouldn't work correctly
        int taskNumberCopy = taskNumber;

        tasks[taskNumber] = Task.Factory.StartNew(
            () =>
            {
                for (int i = array.Length * taskNumberCopy / degreeOfParallelism;
                    i < array.Length * (taskNumberCopy + 1) / degreeOfParallelism;
                    i++)
                {
                    array[i] = array[i] * factor;
                }
            });
    }

    Task.WaitAll(tasks);
}

static void CustomParallelExtractedMax(double[] array, double factor)
{
    var degreeOfParallelism = Environment.ProcessorCount;

    var tasks = new Task[degreeOfParallelism];

    for (int taskNumber = 0; taskNumber < degreeOfParallelism; taskNumber++)
    {
        // capturing taskNumber in lambda wouldn't work correctly
        int taskNumberCopy = taskNumber;

        tasks[taskNumber] = Task.Factory.StartNew(
            () =>
            {
                var max = array.Length * (taskNumberCopy + 1) / degreeOfParallelism;
                for (int i = array.Length * taskNumberCopy / degreeOfParallelism;
                    i < max;
                    i++)
                {
                    array[i] = array[i] * factor;
                }
            });
    }

    Task.WaitAll(tasks);
}

static void CustomParallelExtractedMaxHalfParallelism(double[] array, double factor)
{
    var degreeOfParallelism = Environment.ProcessorCount / 2;

    var tasks = new Task[degreeOfParallelism];

    for (int taskNumber = 0; taskNumber < degreeOfParallelism; taskNumber++)
    {
        // capturing taskNumber in lambda wouldn't work correctly
        int taskNumberCopy = taskNumber;

        tasks[taskNumber] = Task.Factory.StartNew(
            () =>
            {
                var max = array.Length * (taskNumberCopy + 1) / degreeOfParallelism;
                for (int i = array.Length * taskNumberCopy / degreeOfParallelism;
                    i < max;
                    i++)
                {
                    array[i] = array[i] * factor;
                }
            });
    }

    Task.WaitAll(tasks);
}

static void CustomParallelFalseSharing(double[] array, double factor)
{
    var degreeOfParallelism = Environment.ProcessorCount;

    var tasks = new Task[degreeOfParallelism];

    int i = -1;

    for (int taskNumber = 0; taskNumber < degreeOfParallelism; taskNumber++)
    {
        tasks[taskNumber] = Task.Factory.StartNew(
            () =>
            {
                int j = Interlocked.Increment(ref i);
                while (j < array.Length)
                {
                    array[j] = array[j] * factor;
                    j = Interlocked.Increment(ref i);
                }
            });
    }

    Task.WaitAll(tasks);
}
见:

For
循环中,循环体作为委托提供给方法。调用该委托的成本与虚拟方法调用的成本大致相同。在某些场景中,并行循环的主体可能足够小,以至于每次循环迭代中委托调用的成本变得非常大。在这种情况下,您可以使用
Create
重载之一在源元素上创建范围分区的
IEnumerable
。然后,您可以将这个范围集合传递给一个
ForEach
方法,该方法的主体由一个常规for循环组成。这种方法的好处是,委托调用成本在每个范围内只发生一次,而不是每个元素发生一次

在循环体中,您正在执行一次乘法,委托调用的开销将非常明显

试试这个:

public static void MultiplicateArray(double[] array, double factor)
{
    var rangePartitioner = Partitioner.Create(0, array.Length);

    Parallel.ForEach(rangePartitioner, range =>
    {
        for (int i = range.Item1; i < range.Item2; i++)
        {
            array[i] = array[i] * factor;
        }
    });
}
公共静态void MultiplicateArray(双[]数组,双因子)
{
var rangePartitioner=Partitioner.Create(0,array.Length);
Parallel.ForEach(rangePartitioner,range=>
{
对于(int i=range.Item1;i

另请参见:和。

斯维克已经提供了一个很好的答案,但我想强调的是,关键不是“手动并行化代码”,而不是使用
Parallel.For()
,而是您必须处理更大的数据块

这仍然可以使用
Parallel.For()
这样做:

static void My(double[] array, double factor)
{
    int degreeOfParallelism = Environment.ProcessorCount;

    Parallel.For(0, degreeOfParallelism, workerId =>
    {
        var max = array.Length * (workerId + 1) / degreeOfParallelism;
        for (int i = array.Length * workerId / degreeOfParallelism; i < max; i++)
            array[i] = array[i] * factor;
    });
}

顺便说一句,所有其他答案中缺少的关键字是粒度

数组有多大?如果像这样的代码需要几分钟的时间,它们一定很大。难道没有其他方法来并行化代码吗?你的硬件是什么?此外,并行任务对于I/O操作(写入磁盘、调用web服务等)非常有效,而对于“工作者”任务(CPU密集型)@Cybermaxs我认为你错了。并行化CPU密集型任务可能非常有效(如果您有多核计算机)。另一方面,并行写入磁盘很可能会降低性能,而不是提高性能,因为您迫使磁盘在多个位置之间不断搜索。@svick:当然可以!这就是为什么我要问硬件的问题。磁盘IO可能是
static void My(double[] array, double factor)
{
    int degreeOfParallelism = Environment.ProcessorCount;

    Parallel.For(0, degreeOfParallelism, workerId =>
    {
        var max = array.Length * (workerId + 1) / degreeOfParallelism;
        for (int i = array.Length * workerId / degreeOfParallelism; i < max; i++)
            array[i] = array[i] * factor;
    });
}
Serial: 3,94 s
Parallel.For: 9,28 s
Parallel.For (degree of parallelism): 9,58 s
Custom parallel: 2,05 s
Custom parallel (extracted max): 1,19 s
Custom parallel (extracted max, half parallelism): 1,49 s
Custom parallel (false sharing): 27,88 s
My: 0,95 s