OpenMP:在线程之间共享数组

7

大家好!

我正在进行分子动力学模拟,并最近开始尝试并行实现。乍一看,一切都很简单:在最耗时的循环前写上#pragma omp parallel for指令即可。但事实证明,这些循环中的函数操作的是数组,或者更准确地说,操作的是属于我的类对象的数组,该类包含有关粒子系统和作用于该系统的函数的所有信息。因此,当我在其中一个最耗时的循环之前添加了那个#pragma指令时,计算时间实际上增加了数倍,尽管我的2核4线程处理器已经完全负载。

为了解决这个问题,我编写了另一个更简单的程序。这个测试程序执行两个相同的循环,一个并行执行,另一个串行执行。测量执行这两个循环所需的时间。结果让我感到惊讶:每当第一个循环以并行方式计算时,其计算时间与串行模式相比会减少(分别为1500毫秒和6000毫秒),但第二个循环的计算时间却急剧增加(在串行模式下为6000毫秒,在并行模式下为15000毫秒)。

我尝试使用private()和firstprivate()子句,但结果仍然相同。在并行区域之前定义和初始化的每个变量不应该自动共享吗?如果在另一个向量vec2上执行第二个循环,则计算时间会恢复正常,但为每次迭代创建一个新向量显然不是一个选项。我也尝试将vec1的实际更新放入#pragma omp critical区域,但效果也不好。添加Shared(vec1)子句也没有帮助。
如果您能指出我的错误并展示正确的方法,我将不胜感激。
在代码中放置private(i)是否有必要?
以下是此测试程序:
#include "stdafx.h"
#include <omp.h>
#include <array>
#include <time.h>
#include <vector>
#include <iostream>
#include <Windows.h>
using namespace std;
#define N1  1000
#define N2  4000
#define dim 1000

int main(){
    vector<int>res1,res2;
    vector<double>vec1(dim),vec2(N1);
    clock_t t, tt;
    int k=0;
    for( k = 0; k<dim; k++){
        vec1[k]=1;
    }

    t = clock();

    #pragma omp parallel 
        {
        double temp; 
        int i,j,k;
        #pragma omp for private(i)
            for( i = 0; i<N1; i++){
                for(j = 0; j<N2; j++){  
                    for( k = 0; k<dim; k++){
                        temp+= j;
                    }
                }
                vec1[i]+=temp;
                temp = 0;
            }
        }
    tt = clock();
    cout<<tt-t<<endl;
    for(int k = 0; k<dim; k++){
        vec1[k]=1;
    }
    t = clock();
                for(int g = 0; g<N1; g++){
        for(int h = 0; h<N2; h++){
            for(int y = 0; y<dim; y++){
                vec1[g]+=h; 
            }
        }
    }
    tt = clock();
    cout<<tt-t<<endl;
    getchar();
}

感谢您的时间!
附言:我使用Visual Studio 2012,我的处理器是Intel Core i3-2370M。 我的汇编文件分为两部分:

http://pastebin.com/suXn35xj

http://pastebin.com/EJAVabhF


你不需要将i设为私有变量,因为你正在使用#pragma omp for的循环变量会自动被设为私有变量,并且无论如何都在并行部分中定义,因此对于每个线程来说都是私有的。这也适用于jktemp。请注意,当你开始添加到temp时,它是未定义的;你应该将其初始化为零。至于另一个问题——你能确认一下你的意思是,如果你使用多个线程运行并行部分,程序的串行部分运行得更慢吗?如果是这样,那可能是内存亲和性的问题。 - Jonathan Dursi
谢谢您的回复。是的,这是主要问题:当第一部分以并行模式运行时,程序的串行部分需要15,000毫秒才能完成。通常情况下,串行部分只需要大约6,000毫秒。我唯一能够消除这种现象的方法是在程序的第二部分中使用完全不同的向量(在我的实现中为vec2)执行操作。 - Denis
@HristoIliev 很抱歉,我没有复制整个程序,但现在我已经修复了它。我使用的是Visual Studio 2012,我的处理器是Intel Core i3-2370M。 - Denis
然后串行部分的执行时间减少了两个,这表明额外的时间(15,000-6,000毫秒)分布得更或多或少均匀。 - Denis
1
请转到“配置属性”->“C/C++”->“输出文件”,将“汇编器输出”设置为“带源代码的汇编”。然后重建项目并查看发布目录中带有“.asm”结尾的文件。然后将它们的内容粘贴到pastebin中,并将URL添加到您的问题中。在启用OpenMP和禁用OpenMP的情况下执行此操作(请清楚地标记来自哪种情况的文件)。也许我们应该在聊天中继续这个讨论。 - Hristo Iliev
显示剩余13条评论
1个回答

9
恭喜你发现了又一份糟糕的OpenMP实现,感谢Microsoft。我的初步理论是问题出在Sandy Bridge及更高版本的英特尔处理器中分区L3缓存,但仅在向量的前半部分上运行第二个循环的结果未证实该理论。那么问题肯定出在启用OpenMP时所触发的代码生成器中的某些地方。汇编输出确认了这一点。
基本上,当启用OpenMP编译时,编译器不会对串行循环进行优化。这就是减速的原因。问题的一部分也是由于您使第二个循环与第一个不同所造成的。在第一个循环中,你将中间值累积到临时变量中,这使得编译器将其优化为寄存器变量,而在第二个循环中,你在每次迭代中调用operator[]。当你没有启用OpenMP编译时,代码优化器会将第二个循环转换成与第一个循环非常相似的东西,因此你会得到几乎相同的运行时间。
当你启用OpenMP时,代码优化器不会对第二个循环进行优化,它的运行速度会变得非常慢。你的代码在执行之前并没有任何减速的事情。我的猜测是,代码优化器无法理解vec1在OpenMP parallel 区域之外,因此它不应再被视为共享变量,循环可以被优化。显然,这是一个“特性”,它是在Visual Studio 2012中引入的,因为即使启用了OpenMP,Visual Studio 2010中的代码生成器也能对第二个循环进行优化。
一种可能的解决方案是迁移到Visual Studio 2010。另一种(假设性的,因为我没有VS2012)解决方案是将第二个循环提取到函数中,并通过引用传递向量。希望编译器足够聪明,能够优化在单独的函数中的代码。
这是一个非常糟糕的趋势。微软几乎放弃了在Visual C++中支持OpenMP。他们的实现仍然(几乎)仅符合OpenMP 2.0(因此没有显式任务和其他OpenMP 3.0+好处),而像这样的错误并没有使问题得到任何改善。我建议你切换到另一个支持OpenMP的编译器(Intel C/C++编译器、GCC或任何非微软的编译器),或者切换到其他编译器无关的线程化范例,例如Intel线程化建筑块。微软显然正在推动他们的.NET并行库,那就是所有开发工作的地方。

重要警告

不要使用clock()来测量经过的挂钟时间!这只在Windows上按预期工作。在大多数Unix系统(包括Linux)中,clock()实际上返回自创建以来进程中所有线程消耗的总CPU时间。这意味着clock()可能会返回值,这些值比经过的挂钟时间大几倍(如果程序运行了许多繁忙的线程),或者比挂钟时间短几倍(如果程序在测量之间休眠或等待IO事件)。相反,在OpenMP程序中,应该使用可移植的计时器函数omp_get_wtime()


我不知道该如何感谢你为帮助我解决这个问题所付出的所有努力和时间... 将程序中的串行部分制作成外部函数并没有起作用,遗憾。如果起作用的话,就可以更简单地避免这个问题了... 但是你关于operator[]的想法是正确的,在我实现了中间临时变量之后,计算时间确实得到了大幅改善。顺便说一下,我只在程序的第一部分中这样做,以寻找解决第二个循环问题的方法,并且只是因为我注意到了并行性能的一些改进才留下它。 - Denis
@Denis,接受答案就足够了;)实际上,我应该感谢你发布这个问题,因为它让我阅读了一些关于SB架构的工作相关内容,这些内容我本应该在一段时间前就读过,但我一直拖延。它也加强了我对抨击微软进一步破坏他们拥有的如此出色的OpenMP后端的立场,他们的OpenMP前端越来越平庸(好吧,一段时间前后端确实很棒;现在其他人也提供了同样好甚至更好的后端)。 - Hristo Iliev
我不会想象用临时变量替换operator[]会将性能提高三倍(!)。 - Denis
这也表明并行作用域中的代码没有得到优化,因为在那里使用operator[]也会导致计算时间增加(虽然不像串行计算那样剧烈)。 - Denis
1
@Denis,是的,大多数编译器不会优化parallel区域内的代码,因为如果这样做,共享的vec1[i]的中间值将对其他线程不可见,尽管OpenMP标准允许更松散的内存一致性。但是不优化明显位于并行区域之外的代码就太糟糕了。 - Hristo Iliev

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接