不同的执行顺序导致Pthread程序的性能差异

Posted

技术标签:

【中文标题】不同的执行顺序导致Pthread程序的性能差异【英文标题】:Different execution orders cause differences in performance of a Pthread program 【发布时间】:2013-03-28 18:40:11 【问题描述】:

这是我在 *** 上的第一篇文章,我的母语不是英语。这篇文章给您带来的不便请见谅。可能有点长,期待您的耐心等待。提前致谢!

我有一个 C 语言代码 sn-p。这项工作是计算两个文件中的单词数。我使用 pthreads 来解决这个问题。但是我发现这两个语句的顺序

count_words(argv[1]);

pthread_create(&t1, NULL, count_words, (void *)argv[2]);

影响程序性能,这与我的预期相反。代码如下:

#include <stdio.h>
#include <pthread.h>
#include <ctype.h>
#include <stdlib.h>

int total_words;

int main(int argc, char *argv[]) 
    pthread_t t1;
    void *count_words(void *);

    if (argc != 3) 
        printf("usage: %s file1 file2\n", argv[0]);
        exit(1);
    
    total_words = 0;

    count_words(argv[1]); // program runs faster when executing this first
    pthread_create(&t1, NULL, count_words, (void *)argv[2]);

    pthread_join(t1, NULL);
    printf("%5d: total words\n", total_words);
    return 0;


void *count_words(void *f) 
    char *filename = (char *)f;
    FILE *fp;
    int c, prevc = '\0';

    if ((fp = fopen(filename, "r")) == NULL) 
        perror(filename);
        exit(1);
    
    while ((c = getc(fp)) != EOF) 
        if (!isalnum(c) && isalnum(prevc))
            total_words++;
        prevc = c;
    
    fclose(fp);
    return NULL;

性能:

我在命令行上使用“test program_name”运行程序来测试运行速度。输出是:

如果是这样的顺序:

count_words(argv[1]);

pthread_create(&t1, NULL, count_words, (void *)argv[2]);

程序运行速度快:实际0.014s

如果这样:

pthread_create(&t1, NULL, count_words, (void *)argv[2]);

count_words(argv[1]);

程序运行缓慢:实际0.026s

我的预期:

在案例 1 中,程序首先运行 count_word()。完成计数作业后,它会继续运行 pthread_create()。届时,新线程将帮助完成计数工作。因此,新线程在原始线程完成作业后执行作业,这是顺序运行而不是并行运行。在情况 2 中,程序在任何计数之前首先运行 pthread_create(),因此之后有两个并行线程进行计数。所以我希望案例 2 比案例 1 快。但我错了。情况 2 较慢。谁能给我一些有用的信息?

注意

请忽略我没有在全局变量 total_words 上设置互斥锁。这不是我关心的部分。该程序仅用于测试。请原谅它的不完美之处。


编辑 1

以下是我阅读一些建议后的补充和改进。

a) 补充:处理器为 Intel® Celeron(R) CPU 420 @ 1.60GHz。一个核心。

b) 改进:我改进了我的示例,有两个变化:

1) 我放大了文件。 file1是2080651字节(约2M),file2是file1的副本。

2) 我修改了 count_words()。到达文件末尾时,使用 fseek() 将 fp 设置为开头并再次计数。重复计数 COUNT 次。定义 COUNT 20。下面是修改后的代码:

#define COUNT 20

// other unchanged codes ...

void *count_words(void *f) 
    // other unchanged codes ...
  int i;
  for (i = 0; i < COUNT; i++) 
      while ((c = getc(fp)) != EOF) 
          if (!isalnum(c) && isalnum(prevc))
              total_words++;
          prevc = c;
      
      fseek(fp, 0, SEEK_SET);
  
  fclose(fp);
  return NULL;

fast_version(先count_word())和slow_version(先pthread_create())的输出:

administrator@ubuntu:~$ time ./fast_version file1 file2

12241560:总字数

真正的 0m5.057s

用户 0m4.960s

系统 0m0.048s

administrator@ubuntu:~$ time ./slow_version file1 file2

12241560:总字数

真正的 0m7.636s

用户 0m7.280s

系统 0m0.048s

我尝试了几次“time progname file1 file2”命令。每次运行可能会有十分之一或百分之一秒的差异。但差别不大。

编辑 2

这部分是我根据一些提示做了一些实验后添加的--

当您在第一个线程完成执行后启动第二个线程时,没有上下文切换开销。

--by user315052.

实验是我改进了count_word():

void *count_word(void *f) 
// unchanged codes
// ...
    for (i = 0; i < COUNT; i++) 
        while ((c = getc(fp)) != EOF) 
            if (!isalnum(c) && isalnum(prevc))
                total_words++;
            prevc = c;
        
        fseek(fp, 0, SEEK_SET);
        printf("from %s\n", filename); // This statement is newly added.
    
// unchanged codes
// ...

添加语句 " printf("from %s\n", filename); " ,这样我就可以知道当时哪个文件(或线程)正在运行。快速版的输出是20次“from file1”,然后是20次“from file2”,慢速版是“from file1”和“from file2”混合打印。

看起来快速版本更快,因为没有上下文切换。但事实是,count_word() 完成后,原来的线程并没有死掉,而是创建了一个新线程,等待它终止。新线程运行时是否没有上下文切换?我仔细观察了屏幕,发现“from file2”的打印速度明显比“from file1”慢。为什么?是因为从file2计数时发生了上下文切换吗?

对于慢速版本,我们可以从输出中看到“from file1”和“from file2”的打印速度甚至比“from file2”在快速版本的打印速度还要慢,因为它的上下文切换花费了更多时间在并行计数上,而在快速版本中,上下文切换并不那么繁重,因为其中一个线程已完成其工作并只是等待。

所以我认为主要原因是快速版本相对于慢速版本具有轻松轻松的上下文切换。但是“打印速度”是我观察到的,可能没有那么严格。所以我不确定。

【问题讨论】:

你在什么机器上运行?你真的有两个核心吗? 如果你把total_words++注释掉怎么办?相对时间是否保持不变? 时间很短——如何准确测量?更好地运行大量输入。 编译时是否激活了优化选项?如果没有,则 gcc 默认为 -O2。我为这两个示例禁用了优化 (-O0),而且我的时间几乎相同。 @user315052 处理器是 Intel® Celeron(R) CPU 420 @ 1.60GHz。一个核心。 【参考方案1】:

在评论中,您写道:

处理器是 Intel® Celeron(R) CPU 420 @ 1.60GHz。一个核心。

由于您只有一个内核,因此您的线程执行无论如何都会被序列化。您的程序具有两个并发运行的线程,因为每个线程都执行阻塞 I/O,因此需要支付线程上下文切换的开销。

当您在第一个线程完成执行后启动第二个线程时,没有上下文切换开销。

【讨论】:

很好的答案 - 这几乎可以肯定地解释了 OP 对时间的混淆。 +1 感谢您提供的信息!按照您的提示,我做了一个实验并更新了我的帖子。现在我认为你的答案可能不是那么准确。第一个 count_word() (不是第一个线程!)完成后似乎有上下文切换。而且我认为原因是慢版本中上下文切换的成本比快速版本更昂贵。但我不确定。你有什么看法? @WenhaoYang:我所说的“没有上下文切换开销”的意思是每个线程都可以运行完成,而无需在它们之间来回切换。是的,快速版本中会有两个。切换到第二个线程,并在加入后切换回来。除了上下文切换本身的成本之外,上下文切换还存在相关成本,例如中断数据缓存。慢速版本肯定是两次以上的上下文切换,而且每次都会破坏数据缓存。 @WenhaoYang:为了验证我的解释,您可以再创建两个版本的代码。一种是将count_words() 中的main() 的调用移到pthread_join() 之后。那应该还是很快的。第二个是摆脱你的pthread...() 调用,只需在main() 中调用count_words() 两次。这应该给您几乎与您的快速版本相同的时间。 @user315052:你是对的。没有上下文来回切换。但是您建议的实验并没有像您预期的那样运行。我也无法理解。所以我改进了我的实验,使用计时功能使其更严格,并发送一个新的post。您可以查看我的新帖子以了解我的另一个问题。感谢您的帮助!【参考方案2】:

尝试进行相同的测量,但将程序运行 100 次并计算平均时间,例如,在如此短的时间内缓存的影响远不能忽略。

【讨论】:

是的。我编辑了代码。让 count_word() 对同一个文件重复计数 20 次。现在我有更长的时间。但是很抱歉,我只是输入了几次“time progname file1 file2”命令并观察了结果。我发现差异不大,对我来说已经足够了。所以我没有计算平均时间。【参考方案3】:

你是如何测量的?

实时并不表示您的程序运行了多长时间。您必须测量用户+系统时间。更重要的是,毫秒级别的有意义的计时在很大程度上取决于您的计时时钟的粒度。如果它以 60Hz 的频率运行,那么你就有问题了。 提出有意义的基准是一门艺术...

首先,您应该找到一种方法来循环运行线程,例如 10.000 次并将数字相加。这至少可以让你摆脱毫秒计时问题。

【讨论】:

听从您的建议,我更改了代码,放大了文件并编辑了我的帖子。就像我在编辑后的帖子中所说,当 count_words() 到达文件末尾时,返回开始,再次计数。此过程运行 20 次。我还发布了用户和系统时间。也许测量不是很严格也不是很精确。但我们可以得出结论,快的仍然是快的,对吧?

以上是关于不同的执行顺序导致Pthread程序的性能差异的主要内容,如果未能解决你的问题,请参考以下文章

为啥以不同的顺序运行我的测试会导致性能截然不同? [复制]

linux下c程序 daemonfork与创建pthread的顺序问题

不同智能优化算法如何进行性能分析比较?

重排序

SQLITE 3.7.13 和 3.8.0 之间的性能差异

互斥量和执行顺序