【问题标题】:triangular matrix conversion and auto parallelization三角矩阵转换和自动并行化
【发布时间】:2012-04-27 11:38:04
【问题描述】:

我在 ICC(11.1;旧,但对此无能为力)中玩了一点自动并行化,我想知道为什么编译器不能并行化内部循环以进行简单的高斯消除:

void makeTriangular(float **matrix, float *vector, int n) {
    for (int pivot = 0; pivot < n - 1; pivot++) {
        // swap row so that the row with the largest value is
        // at pivot position for numerical stability
        int swapPos = findPivot(matrix, pivot, n);
        std::swap(matrix[pivot], matrix[swapPos]);
        std::swap(vector[pivot], vector[swapPos]);
        float pivotVal = matrix[pivot][pivot];
        for (int row = pivot + 1; row < n; row++) { // line 72; should be parallelized
            float tmp = matrix[row][pivot] / pivotVal;  
            for (int col = pivot + 1; col < n; col++) { // line 74
                matrix[row][col] -= matrix[pivot][col] * tmp;
            }
            vector[row] -= vector[pivot] * tmp;
        }
    }
}

我们只写入依赖于私有行(和 col)变量的数组,并且行保证大于枢轴,因此编译器应该很明显我们没有覆盖任何内容。

我正在使用-O3 -fno-alias -parallel -par-report3 进行编译,并获得了很多依赖项ala:assumed FLOW dependence between matrix line 75 and matrix line 73.assumed ANTI dependence between matrix line 73 and matrix line 75.,仅第75 行也是如此。编译器有什么问题?显然,我可以准确地告诉它如何处理一些 pragma,但我想了解编译器可以单独获得什么。

【问题讨论】:

  • 对我来说,似乎编译器尝试并行化此代码时,它不仅考虑内存位置,还考虑寄存器机器的位置。我不是“编译器”:)。非常深刻的问题。
  • @parallelgeek 不是真的。循环迭代之间的内存依赖是一个问题的原因是因为并行化会改变这种情况下的语义(例如,我是否在另一个线程写入值之前/之后读取)。每个核心都有自己的寄存器集,所以这不是问题。
  • 可能是因为内部循环没有“完美嵌套”?我只是好奇! :) 我看不到任何真正的依赖关系,在更新尾随子矩阵行的情况下限制并行化。但是编译器可能对不完全适合的嵌套更加保守。 :)
  • @parallel 也不确定 icc 如何并行化事物,所以这个问题基本上是一个尝试找出答案 :) 哦,好吧,几百个代表赏金应该尽快帮助事情。
  • 你真正的任务是什么?为什么要讨论这个例子?您只是想为不同的代码挖掘编译器的东西,还是需要优化这个?也许等离子(MAGMA)库适合?

标签: c++ optimization compiler-optimization icc


【解决方案1】:

由于名称matrix 和名称vector 也同时被读取和写入(即使使用不同的区域),基本上编译器无法确定没有依赖关系。您也许可以通过以下方式解决这个问题(虽然有点脏):

void makeTriangular(float **matrix, float *vector, int n)
{     
    for (int pivot = 0; pivot < n - 1; pivot++) 
    {         
         // swap row so that the row with the largest value is    
         // at pivot position for numerical stability       
         int swapPos = findPivot(matrix, pivot, n);    
         std::swap(matrix[pivot], matrix[swapPos]);   
         std::swap(vector[pivot], vector[swapPos]);     
         float pivotVal = matrix[pivot][pivot];     
         float **matrixForWriting = matrix;  // COPY THE POINTER
         float *vectorForWriting = vector;   // COPY THE POINTER
         // (then parallelize this next for loop as you were)
         for (int row = pivot + 1; row < n; row++)  { 
              float tmp = matrix[row][pivot] / pivotVal;               
              for (int col = pivot + 1; col < n; col++) {
                  // WRITE TO THE matrixForWriting VERSION
                  matrixForWriting[row][col] = matrix[row][col] - matrix[pivot][col] * tmp; 
              } 
              // WRITE TO THE vectorForWriting VERSION
              vectorForWriting[row] = vector[row] - vector[pivot] * tmp; 
         } 
    }
} 

底线只是给你正在写的那些暂时不同的名字来欺骗编译器。我知道这有点脏而且我一般不会推荐这种编程方式。但是如果你确定你没有数据依赖,那很好。

事实上,我会在它周围放置一些 cmets,以便未来看到此代码的人非常清楚这是一种解决方法,以及您这样做的原因。

编辑:我认为@FPK 基本上触及了答案,@Evgeny Kluev 发布了答案。但是,在@Evgeny Kluev 的回答中,他建议将此作为输入参数,这可能会并行化但不会给出正确的值,因为matrix 中的条目不会被更新。我认为我上面发布的代码也会给出正确的答案。

【讨论】:

  • 是的,没有理由在此处添加输入参数,因为似乎fno-alias 推翻了所有内容,即使别名很明显 - 很好,尽管仍然非常糟糕。请注意,Evgeny 想要使用输入参数的方式应该能给出正确的答案,就我所见,但尽可能将变量定义为局部是好的风格,所以这样会更好。
  • icc 12.1(Linux、64 位和相同的编译器选项)不会并行化此代码,并给出与 OP 中的代码完全相同的解释。我认为,编译器的优化器只是将 'matrixForWriting' 和 'matrix' 合并回单个变量。
  • @Evgeny 奇怪,在阅读了您的帖子后,我自己编写了基本相同的版本,而带有fno-alias 的 icc 12.1(尽管这两个版本都不适用于 11.1)确实可以正确并行化。
  • 无论如何,只处理局部变量需要击败优化器和自动并行器。很难确定什么时候会并行化,什么时候不会并行化。只是骇人听闻...
  • 我通过这个额外的技巧强制此代码自动并行:float **matrixForWriting = (float**)((unsigned long long)((char*)matrix + 4)) - 1;
【解决方案2】:

icc 12.1 上存在相同的自动并行化问题。所以我用这个较新的版本进行实验。

将输出矩阵添加到函数的参数列表并将第三个循环的主体更改为此

out[row][col] = matrix[row][col] - matrix[pivot][col] * tmp;

修复了“FLOW 依赖”问题。这意味着,“-fno-alias”只影响函数参数,而单个参数的内容仍然被怀疑是别名。我不知道为什么这个选项不会影响一切。由于矩阵的不同部分并没有真正相互别名,您可以将这个附加参数留给函数并通过这个参数传递相同的矩阵。

有趣的是,在抱怨 'matrix' 时,编译器对 'vector' 只字未提,这确实存在别名问题:这条线 vector[row] -= vector[pivot] * tmp; 可能会导致错误别名(在一个线程中写入 vector[row] 可能会触及缓存线,存储vector[pivot],供每个线程使用)。

“FLOW 依赖”不是这段代码中唯一的问题。修复后,编译器仍然拒绝并行化第二个和第三个循环,因为“计算工作不足”。所以我试着给它一些额外的工作:

float tmp = matrix[row][pivot] * pivotVal;
...
out[row][col] = matrix[row][col] - matrix[pivot][col] *tmp /pivotVal /pivotVal;

在这一切之后,第二个循环终于并行化了,虽然我不确定它是否获得了任何速度提升。


更新:我找到了一个更好的替代方案,可以让计算机“做一些额外的工作”。选项-par-threshold50 可以解决问题。

【讨论】:

  • 实际上我正在做任意精度的数学运算,所以还有更多可用的工作。关于向量问题:我认为因为没有真正的别名发生,编译器有责任确保没有问题发生(我假设基本上是 x86 的 lock 前缀)。在我自己的 CPU 上使用 12.1 可以很好地工作,遗憾的是英特尔的 ia32 最高只有 11.1,即使使用了这个技巧,编译器也不够聪明。总之很好的体验。你真的赚到了那小小的代表奖金,谢谢!
  • 注意:由于@Chris 扩展了您的解决方案,我想我会给您rep 奖金并接受Chris 的回答,这样你们两个都可以获得比微薄的+10 更多的rep。希望没事。
  • 关于“向量”:假混叠不会带来任何正确性问题,但性能可能会降低。当然,这不会显着降低性能,因为大多数计算都是在“矩阵”而不是“向量”上完成的。
  • 而“向量”问题可能会部分解决:只需在第二个循环之外计算“float pv = vector[pivot]”即可。
【解决方案3】:

我无法访问 icc 来测试我的想法,但我怀疑编译器担心别名:矩阵定义为浮点**:指向浮点数组的指针数组。所有这些指针都可以指向同一个浮点数组,因此并行化这将非常危险。这没有任何意义,但编译器无法知道。

【讨论】:

  • 我专门用fno-alias 编译以避免这个问题,我认为应该足够好不是吗?
  • 我不确定 icc 是否将其视为别名或报告的流量依赖性。但我认为您可以轻松测试我的想法:删除矩阵参数并将其声明为全局数组:float matrix[1000][1000];用于测试。
  • 不,使用全局分配的内存也无济于事,我们得到了同样的依赖关系..
猜你喜欢
  • 1970-01-01
  • 2017-06-17
  • 1970-01-01
  • 1970-01-01
  • 2011-06-16
  • 1970-01-01
  • 1970-01-01
  • 2011-08-27
  • 2016-10-03
相关资源
最近更新 更多