数组内的分段聚合

时间:2014-12-10 18:46:33

标签: c# arrays parallel-processing

我有大量的原始值类型。该阵列实际上是一维的,但在逻辑上代表一个二维场。当您从左向右阅读时,值需要变为(当前单元格的原始值)+(在左侧单元格中计算的结果)。显然除了每行的第一个元素外,它只是原始值。

我已经有了一个实现它的实现,但是在整个数组上完全迭代,对于大型(1M +元素)数组来说非常慢。

给出以下示例数组

0 0 1 0 0
2 0 0 0 3
0 4 1 1 0
0 1 0 4 1

变为

0 0 1 1 1
2 2 2 2 5
0 4 5 6 6
0 1 1 5 6

依此类推,直至有问题的尺寸(1024x1024)

需要更新阵列(理想情况下),但必要时可以使用另一个阵列。内存占用在这里不是问题,但性能至关重要,因为这些阵列有数百万个元素,每秒必须处理数百次。

单个单元格计算似乎不可并行化,因为它们依赖于从左侧开始的值,因此GPU加速似乎是不可能的。我已经调查了PLINQ,但索引的必要条件使得它很难实现。

是否有另一种方法来构建数据以加快处理速度?

如果使用创新的teqnique进行高效的GPU处理是可行的,那么这将是非常可取的,因为这是当前必须从视频卡中拉出并推回到视频卡的纹理数据。

4 个答案:

答案 0 :(得分:3)

正确的编码和对.NET如何知道东西的一些见解也有帮助: - )

在这种情况下适用的一些经验法则:

  1. 如果您可以提示JIT索引将永远不会超出数组的范围,它将删除额外的分支。
  2. 如果它真的很慢(f.ex。> 1秒),你应该只在多个线程中进行矢量化。否则任务切换,缓存刷新等可能只会消耗增加的速度,你最终会变得更糟。
  3. 如果可能,使内存访问可预测,甚至是连续的。如果你需要另一个阵列,那么就这样吧 - 如果不是,那就更喜欢。
  4. 如果您想要速度,请使用尽可能少的IL指令。一般来说,这似乎有效。
  5. 测试多次迭代。单次迭代可能不够好。
  6. 使用这些规则,您可以按如下方式制作一个小测试用例。请注意,我已将赌注提高到4Kx4K,因为1K的速度非常快,你无法测量它: - )

    public static void Main(string[] args)
    {
        int width = 4096;
        int height = 4096;
    
        int[] ar = new int[width * height];
        Random rnd = new Random(213);
        for (int i = 0; i < ar.Length; ++i)
        {
            ar[i] = rnd.Next(0, 120);
        }
    
        // (5)...
        for (int j = 0; j < 10; ++j)
        {
            Stopwatch sw = Stopwatch.StartNew();
    
            int sum = 0;
            for (int i = 0; i < ar.Length; ++i)  // (3) sequential access
            {
                if ((i % width) == 0)
                {
                    sum = 0;
                }
    
                // (1) --> the JIT will notice this won't go out of bounds because [0<=i<ar.Length]
                // (5) --> '+=' is an expression generating a 'dup'; this creates less IL.
                ar[i] = (sum += ar[i]); 
            }
    
            Console.WriteLine("This took {0:0.0000}s", sw.Elapsed.TotalSeconds);
        }
        Console.ReadLine();
    }
    

    其中一次迭代大约需要0.0174秒,因为这是你描述的最坏情况的16倍,我想你的性能问题已经解决了。

    如果你真的想要平行它以使它更快,我想这是可能的,即使你将松开JIT中的一些优化(具体来说:(1))。但是,如果您拥有像大多数人一样的多核系统,那么这些好处可能会超重:

    for (int j = 0; j < 10; ++j)
    {
        Stopwatch sw = Stopwatch.StartNew();
        Parallel.For(0, height, (a) =>
        {
            int sum = 0;
            for (var i = width * a + 1; i < width * (a + 1); i++)
            {
                ar[i] = (sum += ar[i]);
            }
        });
        Console.WriteLine("This took {0:0.0000}s", sw.Elapsed.TotalSeconds);
    }
    

    如果你确实需要性能,可以将其编译为C ++并使用P / Invoke。即使您不使用GPU,我认为SSE ​​/ AVX指令可能已经为您提供了.NET / C#无法获得的显着性能提升。另外我想指出的是,英特尔C ++编译器可以自动对代码进行矢量化 - 甚至是Xeon PHI。如果不付出太多努力,这可能会给你带来很好的性能提升。

答案 1 :(得分:2)

嗯,我对GPU知之甚少,但我认为没有理由不能并行化,因为依赖只是从左到右。

行之间没有依赖关系。

0 0 1 0 0  - process on core1 |
2 0 0 0 3  - process on core1 |
-------------------------------
0 4 1 1 0  - process on core2 |
0 1 0 4 1  - process on core2 |

虽然上述说法并不完全正确。在内存缓存方面,行之间仍存在隐藏的依赖关系。

可能存在缓存垃圾。您可以阅读“缓存虚假共享”,以了解问题,并了解如何克服这一问题。

答案 2 :(得分:0)

正如@Chris Eelmaa告诉你的那样,可以按行执行并行执行。使用Parallel.For可以像这样重写:

static int[,] values = new int[,]{
    {0, 0, 1, 0, 0},
    {2, 0, 0, 0, 3},
    {0, 4, 1, 1, 0},
    {0, 1, 0, 4 ,1}};
static void Main(string[] args)
{
    int rows=values.GetLength(0);
    int columns=values.GetLength(1);
    Parallel.For(0, rows, (row) =>
    {
        for (var column = 1; column < columns; column++)
        {
            values[row, column] += values[row, column - 1];
        }
    });

    for (var row = 0; row < rows; row++)
    {
        for (var column = 0; column < columns; column++)
        {
            Console.Write("{0} ", values[row, column]);
        }
        Console.WriteLine();
    }

所以,正如你的问题所述,你有一个一维数组,代码会更快一点:

static void Main(string[] args)
{
    var values = new int[1024 * 1024];
    Random r = new Random();
    for (int i = 0; i < 1024; i++)
    {
        for (int j = 0; j < 1024; j++)
        {
            values[i * 1024 + j] = r.Next(25);
        }
    }

    int rows = 1024;
    int columns = 1024;

    Stopwatch sw = Stopwatch.StartNew();
    for (int i = 0; i < 100; i++)
    {
        Parallel.For(0, rows, (row) =>
        {
            for (var column = 1; column < columns; column++)
            {
                values[(row * columns) + column] += values[(row * columns) + column - 1];
            }
        });
    }

    Console.WriteLine(sw.Elapsed);
}

但不如GPU那么快。要使用并行GPU处理,您必须在C++ AMP中重写它,或者查看如何将此并行端口移植到cudafy:http://w8isms.blogspot.com.es/2012/09/cudafy-me-part-3-of-4.html

答案 3 :(得分:0)

您也可以将数组存储为锯齿状数组,内存布局也是一样的。所以,而不是,

int[] texture;

你有,

int[][] texture;

将行操作隔离为

private static Task ProcessRow(int[] row)
{
    var v = row[0];
    for (var i = 1; i < row.Length; i++)
    {
        v = row[i] += v;
    }

    return Task.FromResult(true);
}

然后你可以写一个函数,

Task.WhenAll(texture.Select(ProcessRow)).Wait();

如果你想保留一维数组,可以使用类似的方法,只需更改ProcessRow

private static Task ProcessRow(int[] texture, int start, int limit)
{
    var v = texture[start];
    for (var i = start + 1; i < limit; i++)
    {
        v = texture[i] += v;
    }

    return Task.FromResult(true);
}

然后一次,

var rowSize = 1024;
var rows =
    Enumerable.Range(0, texture.Length / rowSize)
    .Select(i => Tuple.Create(i * rowSize, (i * rowSize) + rowSize))
    .ToArray();

然后在每个周期。

Task.WhenAll(rows.Select(t => ProcessRow(texture, t.Item1, t.Item2)).Wait();

无论哪种方式,每一行都是并行处理的。