7

今天是个好日子!

我正在进行分子动力学模拟,最近我开始尝试并行实现它。乍一看,一切看起来都很简单:在最耗时的循环前面编写#pragma omp parallel for 指令。但是碰巧的是,这些循环中的函数在数组上运行,或者更准确地说,在属于我的类的对象的数组上运行,该对象包含有关粒子系统和在该系统上的函数的所有信息,所以当我添加时在最耗时的循环之一之前的 pragma 指令,尽管我的 2 核 4 线程处理器已满载,但计算时间实际上增加了数倍。

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

我尝试使用 private() 和 firstprivate() 子句,但结果是一样的。在并行区域之前定义和初始化的每个变量都不应该自动共享吗?如果在另一个向量 vec2 上执行第二个循环的计算时间会恢复正常,但是为每次迭代创建一个新向量显然不是一种选择。我还尝试将 vec1 的实际更新放入 #pragma omp 关键区域,但这也不是什么好事。两者都没有帮助添加 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();
}

感谢您的时间!

PS我使用Visual Studio 2012,我的处理器是Intel Core i3-2370M。我的程序集文件分为两部分:

http://pastebin.com/suXn35xj

http://pastebin.com/EJAVabhF

4

1 回答 1

9

恭喜!您已经暴露了另一个糟糕的 OpenMP 实现,这是由 Microsoft 提供的。我最初的理论是,问题来自 Sandy Bridge 和后来的 Intel CPU 中的分区 L3 缓存。但是仅在向量的前半部分运行第二个循环的结果并未证实该理论。然后它必须是启用 OpenMP 时触发的代码生成器中的某些内容。汇编输出证实了这一点。

基本上,在启用 OpenMP 的情况下编译时,编译器不会优化串行循环。这就是放缓的来源。通过使第二个循环与第一个循环不同,您自己也引入了部分问题。在第一个循环中,您将中间值累积到一个临时变量中,编译器将其优化为寄存器变量,而在第二种情况下,您operator[]在每次迭代时调用。当您在未启用 OpenMP 的情况下进行编译时,代码优化器会将第二个循环转换为与第一个循环非常相似的内容,因此两个循环的运行时间几乎相同。

当您启用 OpenMP 时,代码优化器不会优化第二个循环并且运行速度会变慢。您的代码在此之前执行并行块的事实与减速无关。我的猜测是代码优化器无法掌握vec1OpenMP 区域范围之外的事实,parallel因此不应再将其视为共享变量,并且可以优化循环。显然,这是 Visual Studio 2012 中引入的“功能”,因为即使启用了 OpenMP,Visual Studio 2010 中的代码生成器也能够优化第二个循环。

一种可能的解决方案是迁移到 Visual Studio 2010。另一种(假设的,因为我没有 VS2012)解决方案是将第二个循环提取到一个函数中并通过引用它来传递向量。希望编译器足够聪明,可以优化单独函数中的代码。

这是一个非常糟糕的趋势。微软实际上已经放弃了在 Visual C++ 中支持 OpenMP。它们的实现仍然(几乎)仅符合 OpenMP 2.0(因此没有明确的任务和其他 OpenMP 3.0+ 好东西),并且像这样的错误不会让事情变得更好。我建议您切换到另一个启用 OpenMP 的编译器(英特尔 C/C++ 编译器、GCC、任何非 Microsoft 的东西)或切换到其他一些独立于编译器的线程范例,例如英特尔线程构建块。微软显然正在推动他们的 .NET 并行库,而这正是所有开发的方向。


大胖警告

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

于 2012-12-17T15:11:29.090 回答