c – 并行for_each比std :: for_each慢两倍以上

我正在读安东尼·威廉姆斯的 C++ Concurrency in Action.在关于设计并发代码的章节中,有 std::for_each algorihtm的并行版本.以下是本书略有修改的代码:

join_thread.hpp

#pragma once

#include <vector>
#include <thread>

class join_threads
{
public:
  explicit join_threads(std::vector<std::thread>& threads)
    : threads_(threads) {}

  ~join_threads()
  {
    for (size_t i = 0; i < threads_.size(); ++i)
    {
      if(threads_[i].joinable())
      {
        threads_[i].join();
      }
    }
  }

private:
  std::vector<std::thread>& threads_;
};

parallel_for_each.hpp

#pragma once

#include <future>
#include <algorithm>

#include "join_threads.hpp"

template<typename Iterator,typename Func>
void parallel_for_each(Iterator first,Iterator last,Func func)
{
  const auto length = std::distance(first,last);
  if (0 == length) return;

  const auto min_per_thread = 25u;
  const unsigned max_threads = (length + min_per_thread - 1) / min_per_thread;

  const auto hardware_threads = std::thread::hardware_concurrency();

  const auto num_threads= std::min(hardware_threads != 0 ?
        hardware_threads : 2u,max_threads);

  const auto block_size = length / num_threads;

  std::vector<std::future<void>> futures(num_threads - 1);
  std::vector<std::thread> threads(num_threads-1);
  join_threads joiner(threads);

  auto block_start = first;
  for (unsigned i = 0; i < num_threads - 1; ++i)
  {
    auto block_end = block_start;
    std::advance(block_end,block_size);
    std::packaged_task<void (void)> task([block_start,block_end,func]()
    {
      std::for_each(block_start,func);
    });
    futures[i] = task.get_future();
    threads[i] = std::thread(std::move(task));
    block_start = block_end;
  }

  std::for_each(block_start,last,func);

  for (size_t i = 0; i < num_threads - 1; ++i)
  {
    futures[i].get();
  }
}

我使用以下程序使用顺序版本std::for_each对其进行基准测试:

main.cpp中

#include <iostream>
#include <random>
#include <chrono>

#include "parallel_for_each.hpp"

using namespace std;

constexpr size_t ARRAY_SIZE = 500'000'000;
typedef std::vector<uint64_t> Array;

template <class FE,class F>
void test_for_each(const Array& a,FE fe,F f,atomic<uint64_t>& result)
{
  auto time_begin = chrono::high_resolution_clock::now();
  result = 0;
  fe(a.begin(),a.end(),f);
  auto time_end = chrono::high_resolution_clock::now();

  cout << "Result = " << result << endl;
  cout << "Time: " << chrono::duration_cast<chrono::milliseconds>(
            time_end - time_begin).count() << endl;
}

int main()
{
  random_device device;
  default_random_engine engine(device());
  uniform_int_distribution<uint8_t> distribution(0,255);

  Array a;
  a.reserve(ARRAY_SIZE);

  cout << "Generating array ... " << endl;
  for (size_t i = 0; i < ARRAY_SIZE; ++i)
    a.push_back(distribution(engine));

  atomic<uint64_t> result;
  auto acc = [&result](uint64_t value) { result += value; };

  cout << "parallel_for_each ..." << endl;
  test_for_each(a,parallel_for_each<Array::const_iterator,decltype(acc)>,acc,result);
  cout << "for_each ..." << endl;
  test_for_each(a,for_each<Array::const_iterator,result);

  return 0;
}

我机器上算法的并行版本比顺序版本慢两倍:

parallel_for_each ...
Result = 63750301073
Time: 5448
for_each ...
Result = 63750301073
Time: 2496

我在运行在Intel(R)Core(TM)i3-6100 CPU @ 3.70GHz上的Ubuntu Linux上使用GCC 6.2编译器.

如何解释这种行为?这是因为共享原子< uint64_t>线程和缓存乒乓之间的变量?

我用perf分别对它们进行了分析.对于并行版本,统计数据如下:

1137982167      cache-references                                            
  247652893      cache-misses              #   21,762 % of all cache refs    
60868183996      cycles                                                      
27409239189      instructions              #    0,45  insns per cycle        
 3287117194      branches                                                    
      80895      faults                                                      
          4      migrations

对于顺序的:

402791485      cache-references                                            
  246561299      cache-misses              #   61,213 % of all cache refs    
40284812779      cycles                                                      
26515783790      instructions              #    0,66  insns per cycle
 3188784664      branches                                                    
      48179      faults
          3      migrations

很明显,并行版本会产生更多的缓存引用,循环和故障,但为什么呢?

解决方法

您正在共享相同的结果变量:所有线程都在atomic< uint64_t>上累积.结果,捶打缓存!

每次线程写入结果时,其他核心中的所有高速缓存都将失效:这会导致高速缓存行争用.

更多信息:

> “Sharing Is the Root of All Contention”.

[…] to write to a memory location a core must additionally have exclusive ownership of the cache line containing that location. While one core has exclusive use,all other cores trying to write the same memory location must wait and take turns — that is,they must run serially. Conceptually,it’s as if each cache line were protected by a hardware mutex,where only one core can hold the hardware lock on that cache line at a time.

> This article on “false sharing”,其中涉及类似问题,更深入地解释了缓存中发生的情况.

我对您的程序进行了一些修改并获得了以下结果(在具有i7-4770K [8线程超线程]的机器上):

Generating array ...
parallel_for_each ...
Result = 63748111806
Time: 195
for_each ...
Result = 63748111806
Time: 2727

并行版本比串行版本快约92%.

> std :: future和std :: packaged_task是重量级抽象.在这种情况下,std::experimental::latch就足够了.
>每个任务都发送到线程池这可以最大限度地减少线程创建开销.
>每个任务都有自己的累加器.这消除了共享.

代码可用于here on my GitHub.它使用了一些个人依赖项,但您应该了解这些更改.

以下是最重要的变化:

// A latch is being used instead of a vector of futures.
ecst::latch l(num_threads - 1);

l.execute_and_wait_until_zero([&]
{
    auto block_start = first;
    for (unsigned i = 0; i < num_threads - 1; ++i)
    {
        auto block_end = block_start;
        std::advance(block_end,block_size);

        // `p` is a thread pool.
        // Every task posted in the thread pool has its own `tempacc` accumulator.
        p.post([&,block_start,tempacc = 0ull]() mutable
        {
            // The task accumulator is filled up...
            std::for_each(block_start,[&tempacc](auto x){ tempacc += x; });

            // ...and then the atomic variable is incremented ONCE.
            func(tempacc);
            l.decrement_and_notify_all();
        });

        block_start = block_end;
    }

    // Same idea here: accumulate to local non-atomic counter,then
    // add the partial result to the atomic counter ONCE.
    auto tempacc2 = 0ull;
    std::for_each(block_start,[&tempacc2](auto x){ tempacc2 += x; });
    func(tempacc2);
});

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


首先GDB是类unix系统下一个优秀的调试工具, 当然作为debug代码的工具, 肯定没有IDE来的直观了. 不过, 命令行自然有命令行的有点, 当你无法是使用IDE时, gdb有时会帮上很大的忙. 下面用1个例子来说明用法吧: 建立1个目录testgdb2 1. 编写c文件test.c可以见到在2
1. C语言定义1个数组的时候, 必须同时指定它的长度.例如:int a[5]={1,2,3,4,5}; //合法int b[6]; //合法int c[]; //错误 因为没有指定长度但是下面语句是正确, 它隐形定义了数组的长度, 就是赋值元素的个数int d[] ={4,5,6,7,8,9} /
C++的auto关键字在C+⬑新标准出来之前基本上处于弃用的状态,到C+⬑新标准发布之后,auto关键字被赋予了新的意义,并且功能也变得很强大,此篇文章重点介绍auto关键字的新功能、新用法,以及在C+⬔、C+⬗、C+⬠各版本中对它的使用缺陷和限制不断地修正和增强,当然auto关
const关键字是用于定义一个不该被改变的对象,它的作用是告诉编译器和其他程序员不允许修改这个对象的值或者状态。当程序员看到使用const修饰的代码时就知道不应该修改对应对象的值,而编译器则会强制实施这个约束,任何违反这个规定的代码会在编译期间报错。它可以用于任何函数或者类之后的全局或namespa
文章浏览阅读315次。之前用C语言编过链表,这几天突然想用C++编一下链表,搞了大半天才搞出来,所以就赶紧整理一下记录下来,省的万一时间长了找不到代码哈哈。一、链表代码1、Node.h文件代码#pragma onceclass Node{public: int ID; char alph; Node* next; Node(int ID,char alph); ~Node();private:..._if(current->id==id)
文章浏览阅读219次。碰到问题就要记录下来,防止遗忘吧。文章目录一、VS中的命令行参数二、内联函数和宏三、初始化和赋值一、VS中的命令行参数今天在运行代码的时候,碰都了下面的情况: // 解析命令行参数 if (pcl::console::find_argument (argc, argv, "-h") >= 0) { printUsage (argv[0]); return 0; }..._"if (pcl::console::find_argument(argc, argv, "-f") >= 0)怎么输入参数"
文章浏览阅读1.8k次,点赞11次,收藏37次。因为自己对决策树的机制非常的好奇,所以就研究了一下决策树的ID3算法,在这也做一篇笔记记录一下过程。文章目录一、什么是决策树?二、信息增益2.1信息熵2.1.1定义2.1.2演变2.2信息增益三、ID3算法实现四、小结一、什么是决策树?这个问题是我从一开始就有的疑问,什么是决策树?在看了一些资料之后,因为没有看到书上给出具体定义,所以按照我自己的理解决策树就是通过一个个“决策”而构建的一种树状结构,而且决策树的整个处理机制非常类似于我们人类在面临决策问题时的处理机制,这也可能就是其名字的由来。决_c++id3
文章浏览阅读492次。C++ 设计模式之策略模式
文章浏览阅读683次。我也算是个C++的小白,对于C++中的谓语我第一时间就想到了C#中的委托,但两者又不尽相同,所以想写一篇笔记记录一下。文章目录一、什么是谓语?二、使用谓语一、什么是谓语?谓语是一个可调用的表达式,其返回的结果可以作为条件的值,在C++中其实就是向算法传递函数。这和C#中的委托的概念其实是一样的,都是将函数作为参数进行传递。C++标准库中的谓语主要有两类:一元谓语和二元谓语,也就是有的算法只能..._谓语句 c++
文章浏览阅读225次。又看了一遍操作符的东西,感觉之前对操作符的理解还停留在很浅的认知上(仅仅会用哈哈),所以做一下笔记来加深一下印象。文章目录一、为什么会有操作符重载?二、操作符重载作用的对象一、为什么会有操作符重载?如果要回答这个问题,我们其实应该仔细想一下如果没有操作符重载会怎样呢?这其实很容易就联想到了C语言,因为他就没有操作符重载这一说。虽然C语言中没有类class这一概念,但是他有着和类及其相似的结构..._6-6 我的朋友 - c/c++ 操作符重载分数 15作者 海洋饼干叔叔单位 重庆大学实现frie
文章浏览阅读216次。因为之前碰到了很多关于C++上的问题,现在整理并记录一下。文章目录一、引用一、引用在C++中,引用就是给对象起了另一个名字,也就是“对象别名”。感觉和什么东西很相似,仔细一想不就是类型别名“typedef”吗哈哈。它其实是和原对象形成了一种绑定的一种关系,..._vc++6.0报错:returning address of local
文章浏览阅读565次。因为一直好奇预处理器的工作机制,所以就查了查书,做一下自己看完书之后的笔记。文章目录一、预处理器的作用一、预处理器的作用_c语言预处理器作用
文章浏览阅读1.8k次,点赞3次,收藏10次。最近特别查阅了一下关于C++文件的输入/输出的资料,整理了一下就写一下笔记。文章目录一、什么是流二、什么是缓冲区三、代码实现文件IO3.1 使用文件流对象读取数据3.2重定向一、什么是流当前的计算机具有很多种设备,但是无论是哪种设备都要与数据和信息进行打交道,所以这就牵扯到设备与数据之间的I/O操作。而每种设备又有着不同的特性和操作协议,由于过于复杂,所以我们一般是不会和这些通信细节打交道的..._c++ inpath
文章浏览阅读4.8k次,点赞6次,收藏29次。因为要使用到C++的动态链接库,所以就特意网上找了一下资料实现了一下。文章目录一、lib与dll文件二、创建dll文件三、dll隐式链接四、显式链接五、小结一、lib与dll文件之前我一直以为动态链接库就是指dll文件,这也是C#给我造成的一种印象,因为在C#中建立的类库文件都是dll文件,而且只要简单引用就可以了,但是C++却并不是这样的,这可能是因为C#隐藏了一些细节的缘故吧。在C++中共有两种库模式,一种是包含lib和dll两种文件,这种情况下其中的lib文件包含了函数所在的dll文件和dl_c++调用动态链接库
文章浏览阅读973次。因为遇到了一这个操作符的问题,所以记录一下出现的问题*~*。一、问题描述二、产生原因因为也是第一次出现这个问题,所以就到网上查了一些资料和书籍,现在倒也大概理解这个错误出现的原因了。有时候举个例子可能更容易理解为啥会出现这个错误,就拿一本书中的例子来说一下,如下所示:template<class T> class NamedObject { public: NamedObject(std::string& nameVal, const T objectVal) __copy_assign报错
C语言中的单向链表可以解决数组和结构体在使用时的内存连续性问题,同时还能动态地调整长度。本文介绍了单向链表的结构和基本操作,并给出了一个简单的示例代码。
文章浏览阅读2.3k次。区分'0'、"0"、0、''_0和
文章浏览阅读5.8k次,点赞4次,收藏8次。C语言函数指针详解,微剖本质_c语言指针函数
数组指针和指针数组是代码中常见的定义形式。虽然它们的语法类似,但含义完全不同。对于一维数组而言,数组名即为首元素的地址,不需要取址即可赋值给指针。而对于二维数组,数组名代表首行元素的地址,可以看作是一个指针数组,需要使用取址操作。
文章浏览阅读297次。总结刚入门的新同学C语言编程常见的低级错误