【问题标题】:Why is my computer not showing a speedup when I use parallel code?为什么我的计算机在使用并行代码时没有显示加速?
【发布时间】:2010-03-08 23:01:08
【问题描述】:

所以我意识到这个问题听起来很愚蠢(是的,我使用的是双核),但是我尝试了两个不同的库(Grand Central Dispatch 和 OpenMP),并且在使用 clock() 对代码进行计时时使用和不使用使其平行的线,速度是相同的。 (为了记录,他们都使用自己的并行形式)。他们报告在不同的线程上运行,但也许他们在同一个核心上运行?有什么方法可以检查吗? (这两个库都是用于 C 的,我在较低层感到不舒服。)这非常奇怪。有什么想法吗?

【问题讨论】:

  • 这在很大程度上取决于您正在运行的代码类型以及它在做什么。此外,还需要考虑规模问题 - 大多数并行库都有某种形式的设置开销,至少对于小程序而言,这些开销可以支配程序的实际运行时间。

标签: c parallel-processing openmp performance grand-central-dispatch


【解决方案1】:

编辑:为响应 OP 评论添加了 Grand Central Dispatch 的详细信息。

虽然此处的其他答案通常很有用,但您的问题的具体答案是您不应该使用 clock() 来比较时间。 clock() 测量跨线程累加的 CPU 时间。当您在内核之间拆分作业时,它至少会使用尽可能多的 CPU 时间(由于线程开销,通常会多一点)。在this页面上搜索clock(),找到“如果进程是多线程的,则添加进程的所有单个线程消耗的cpu时间”。

只是作业在线程之间进行拆分,因此您必须等待的总时间更少。您应该使用挂钟时间(挂钟上的时间)。 OpenMP 提供了一个例程omp_get_wtime() 来执行此操作。以如下例程为例:

#include <omp.h>
#include <time.h>
#include <math.h>
#include <stdio.h>

int main(int argc, char *argv[]) {
    int i, nthreads;
    clock_t clock_timer;
    double wall_timer;
    for (nthreads = 1; nthreads <=8; nthreads++) {
        clock_timer = clock();
        wall_timer = omp_get_wtime();
        #pragma omp parallel for private(i) num_threads(nthreads)
        for (i = 0; i < 100000000; i++) cos(i);
        printf("%d threads: time on clock() = %.3f, on wall = %.3f\n", \
            nthreads, \
            (double) (clock() - clock_timer) / CLOCKS_PER_SEC, \
            omp_get_wtime() - wall_timer);
    }
}

结果是:

1 threads: time on clock() = 0.258, on wall = 0.258
2 threads: time on clock() = 0.256, on wall = 0.129
3 threads: time on clock() = 0.255, on wall = 0.086
4 threads: time on clock() = 0.257, on wall = 0.065
5 threads: time on clock() = 0.255, on wall = 0.051
6 threads: time on clock() = 0.257, on wall = 0.044
7 threads: time on clock() = 0.255, on wall = 0.037
8 threads: time on clock() = 0.256, on wall = 0.033

您可以看到clock() 时间没有太大变化。如果没有pragma,我得到 0.254,因此使用带有一个线程的 openMP 比完全不使用 openMP 慢一点,但是每个线程的挂墙时间都会减少。

由于某些计算部分不并行(请参阅Amdahl's_law)或不同线程争夺相同内存等原因,改进并不总是那么好。

编辑:对于 Grand Central Dispatch,GCD reference 声明 GCD 使用 gettimeofday 进行挂壁时间。因此,我创建了一个新的 Cocoa 应用程序,并在 applicationDidFinishLaunching 中输入:

struct timeval t1,t2;
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
for (int iterations = 1; iterations <= 8; iterations++) {
    int stride = 1e8/iterations;
    gettimeofday(&t1,0);
    dispatch_apply(iterations, queue, ^(size_t i) { 
        for (int j = 0; j < stride; j++) cos(j); 
    });
    gettimeofday(&t2,0);
    NSLog(@"%d iterations: on wall = %.3f\n",iterations, \
                t2.tv_sec+t2.tv_usec/1e6-(t1.tv_sec+t1.tv_usec/1e6));
}

我在控制台上得到以下结果:

2010-03-10 17:33:43.022 GCDClock[39741:a0f] 1 iterations: on wall = 0.254
2010-03-10 17:33:43.151 GCDClock[39741:a0f] 2 iterations: on wall = 0.127
2010-03-10 17:33:43.236 GCDClock[39741:a0f] 3 iterations: on wall = 0.085
2010-03-10 17:33:43.301 GCDClock[39741:a0f] 4 iterations: on wall = 0.064
2010-03-10 17:33:43.352 GCDClock[39741:a0f] 5 iterations: on wall = 0.051
2010-03-10 17:33:43.395 GCDClock[39741:a0f] 6 iterations: on wall = 0.043
2010-03-10 17:33:43.433 GCDClock[39741:a0f] 7 iterations: on wall = 0.038
2010-03-10 17:33:43.468 GCDClock[39741:a0f] 8 iterations: on wall = 0.034

这和我上面说的差不多。

这是一个非常人为的例子。事实上,您需要确保将优化保持在 -O0,否则编译器将意识到我们不保留任何计算并且根本不执行循环。此外,在两个示例中,我采用的 cos 的整数不同,但这不会对结果产生太大影响。请参阅手册页上的STRIDE 了解dispatch_apply 的正确操作方法以及为什么iterations 在这种情况下与num_threads 大致相当。

编辑:我注意到雅各布的回答包括

我使用 omp_get_thread_num() 在我的并行循环中运行 打印出它正在工作的核心 on... 这样你就可以确定 它在两个内核上运行。

这是不正确的(已通过编辑部分修复)。使用omp_get_thread_num() 确实是确保您的代码是多线程的好方法,但它不会显示“它正在处理哪个内核”,而只是显示哪个线程。例如以下代码:

#include <omp.h>
#include <stdio.h>

int main() {
    int i;
    #pragma omp parallel for private(i) num_threads(50)
    for (i = 0; i < 50; i++) printf("%d\n", omp_get_thread_num());
}

打印出它正在使用线程 0 到 49,但这并没有显示它正在处理哪个内核,因为我只有八个内核。通过查看活动监视器(OP 提到了 GCD,所以必须在 Mac 上 - 转到 Window/CPU Usage),您可以看到作业在内核之间切换,因此 core != thread。

【讨论】:

  • 该死的好点。我通常使用 RDTSC 进行分析,而不是 clock()。
  • 谢谢 这听起来像我在找的东西,但你能解释一下吗?就像我在 for 循环之外使用时钟一样(在除调用线程之外的所有线程都完成之后),这意味着 clock() 对于获取时间不可靠。我不太确定这听起来是否正确。我关心代码运行所需的时间本身(以毫秒或其他为单位),而不关心任何其他统计数据。另外,omp_get_wtime 的 GCD 版本是什么?
  • clock() 对于获取所有线程使用的时间的 CPU 时间是可靠的。 GCD 使用gettimeofday 作为挂墙时间,请参阅上面的详细信息和参考资料。
  • 遗憾的是,gettimeofday 并不总能提供分析小块代码所需的分辨率。这就是我倾向于使用 RDTSC 的原因。否则,这个建议是无效的。
  • @T.E.D.:好的。我使用@FreeMemory 对stackoverflow.com/questions/638269/… 的回答中的计时器重新运行了结果(我注意到您对此进行了评论)并获得了一致的结果(大约3sf,或毫秒级)。在en.wikipedia.org/wiki/Time%5FStamp%5FCounter 对 RDTSC 对多核的批评有什么问题吗?顺便说一句,如果差异低于gettimeofday 提供的微秒级别,则分析结果在运行之间往往会非常不同(取决于缓存、我的计算机正在做什么等)。
【解决方案2】:

您的执行时间很可能不受您并行化的那些循环的限制。

我的建议是您分析您的代码以查看大部分时间花费的时间。大多数工程师会告诉您,您应该做任何剧烈的优化事情之前这样做。

【讨论】:

    【解决方案3】:

    没有任何细节很难猜测。也许您的应用程序甚至不受 CPU 限制。您是否在代码运行时观察 CPU 负载?它是否在至少一个核心上达到 100%?

    【讨论】:

      【解决方案4】:

      您的问题缺少一些非常重要的细节,例如您的应用程序的性质、您要改进的部分、分析结果(如果有的话)等...

      在进行性能改进工作时,您应该记住几个关键点:

      • 工作应始终集中在已被通过分析证明效率低下的代码区域
      • 并行化 CPU 绑定代码几乎永远不会提高性能(在单核机器上)。您将在不必要的上下文切换上浪费宝贵的时间,而一无所获。通过这样做,您可以很容易地降低性能
      • 即使您在多核计算机上并行处理 CPU 绑定代码,您也必须记住,您永远无法保证并行执行。

      确保你没有违背这些观点,因为有根据的猜测(除非有任何额外的细节)会说这正是你正在做的事情。

      【讨论】:

      • 您的第 2 点不太正确。通常,当程序员构建并行算法时,他们自然会提高被并行化代码的局部性。本质上,它们为了并行而阻塞循环,然后它们的顺序执行也变得更快。
      【解决方案5】:

      如果您在循环中使用大量内存,这可能会阻止它更快。您也可以查看 pthread 库,手动处理线程。

      【讨论】:

        【解决方案6】:

        如果您不指定num_threads,我在并行循环中使用omp_get_thread_num() 函数来打印出它正在处理哪个内核。例如,

        printf("Computing bla %d on core %d/%d ...\n",i+1,omp_get_thread_num()+1,omp_get_max_threads());
        

        以上内容适用于此编译指示 #pragma omp parallel for default(none) shared(a,b,c)

        这样您可以确保它在两个内核上运行,因为只会创建 2 个线程。

        顺便说一句,您在编译时是否启用了 OpenMP?在 Visual Studio 中,您必须在 Property PagesC++ -&gt; Language 中启用它并将 OpenMP Support 设置为 Yes

        【讨论】:

        • 这显示了哪个线程在做这项工作,而不是哪个核心 - 我上面的回答提供了详细信息。
        猜你喜欢
        • 1970-01-01
        • 2018-10-12
        • 2021-01-03
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-11-27
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多