性能损失并行



我有一个程序,它或多或少地重复执行一些向量运算。当我尝试使用parallel_for并行执行相同的任务时,我观察到每个任务的时间显着增加。每个任务从相同的数据读取,并且没有进行同步。下面是示例代码(它需要 Taskflow 库 (https://github.com/cpp-taskflow/cpp-taskflow(:

#include <array>
#include <numeric>
#include <x86intrin.h>
#include "taskflow.hpp"
//#define USE_AVX_512 1
constexpr size_t Size = 5000;
struct alignas(64) Vec : public std::array<double, Size> {};
struct SimulationData
{
Vec a_;
Vec b_;
Vec c_;
SimulationData()
{
std::iota(a_.begin(), a_.end(), 10);
std::iota(b_.begin(), b_.end(), 5);
std::iota(c_.begin(), c_.end(), 0);
}
};
struct SimulationTask
{
const SimulationData& data_;
double res_;
double time_;
explicit SimulationTask(const SimulationData& data)
: data_(data), res_(0.0), time_(0.0)
{}
constexpr static int blockSize = 20000;
void sample()
{
auto tbeg = std::chrono::steady_clock::now();
Vec result;
for(auto i=0; i < blockSize; ++i)
{
add(result.data(), data_.a_.data(), data_.b_.data(), Size);
mul(result.data(), result.data(), data_.c_.data(), Size);
res_ += *std::max_element(result.begin(), result.end());
}
auto tend = std::chrono::steady_clock::now();
time_ = std::chrono::duration_cast<std::chrono::milliseconds>(tend-tbeg).count();
}
inline double getResults() const
{
return res_;
}
inline double getTime() const
{
return time_;
}
static void add( double* result, const double* a, const double* b, size_t size)
{
size_t i = 0;
// AVX-512 loop
#ifdef USE_AVX_512
for( ; i < (size & ~0x7); i += 8)
{
const __m512d kA8   = _mm512_load_pd( &a[i] );
const __m512d kB8   = _mm512_load_pd( &b[i] );
const __m512d kRes = _mm512_add_pd( kA8, kB8 );
_mm512_stream_pd( &result[i], kRes );
}
#endif
// AVX loop
for ( ; i < (size & ~0x3); i += 4 )
{
const __m256d kA4   = _mm256_load_pd( &a[i] );
const __m256d kB4   = _mm256_load_pd( &b[i] );
const __m256d kRes = _mm256_add_pd( kA4, kB4 );
_mm256_stream_pd( &result[i], kRes );
}
// SSE2 loop
for ( ; i < (size & ~0x1); i += 2 )
{
const __m128d kA2   = _mm_load_pd( &a[i] );
const __m128d kB2   = _mm_load_pd( &b[i] );
const __m128d kRes = _mm_add_pd( kA2, kB2 );
_mm_stream_pd( &result[i], kRes );
}
// Serial loop
for( ; i < size; i++ )
{
result[i] = a[i] + b[i];
}
}
static void mul( double* result, const double* a, const double* b, size_t size)
{
size_t i = 0;
// AVX-512 loop
#ifdef USE_AVX_512
for( ; i < (size & ~0x7); i += 8)
{
const __m512d kA8   = _mm512_load_pd( &a[i] );
const __m512d kB8   = _mm512_load_pd( &b[i] );
const __m512d kRes = _mm512_mul_pd( kA8, kB8 );
_mm512_stream_pd( &result[i], kRes );
}
#endif
// AVX loop
for ( ; i < (size & ~0x3); i += 4 )
{
const __m256d kA4   = _mm256_load_pd( &a[i] );
const __m256d kB4   = _mm256_load_pd( &b[i] );
const __m256d kRes = _mm256_mul_pd( kA4, kB4 );
_mm256_stream_pd( &result[i], kRes );
}
// SSE2 loop
for ( ; i < (size & ~0x1); i += 2 )
{
const __m128d kA2   = _mm_load_pd( &a[i] );
const __m128d kB2   = _mm_load_pd( &b[i] );
const __m128d kRes = _mm_mul_pd( kA2, kB2 );
_mm_stream_pd( &result[i], kRes );
}
// Serial loop
for( ; i < size; i++ )
{
result[i] = a[i] * b[i];
}
}
};
int main(int argc, const char* argv[])
{
int numOfThreads = 1;
if ( argc > 1 )
numOfThreads = atoi( argv[1] );
try
{
SimulationData data;
std::vector<SimulationTask> tasks;
for (int i = 0; i < numOfThreads; ++i)
tasks.emplace_back(data);
tf::Taskflow tf;
tf.parallel_for(tasks, [](auto &task) { task.sample(); });
tf.wait_for_all();
for (const auto &task : tasks)
{
std::cout << "Result: " << task.getResults() << ", Time: " << task.getTime() << std::endl;
}
}
catch (const std::exception& ex)
{
std::cerr << ex.what() << std::endl;
}
return 0;
}

我在双 E5-2697 v2 上使用g++-8.2 -std=c++17 -mavx -o timing -O3 timing.cpp -lpthread编译了此代码(每个 CPU 都有 12 个具有超线程的物理内核,因此有 48 个硬件线程可用(。当我增加并行任务的数量时,每个任务的时间都会增加很多:

# ./timing 1
Result: 1.0011e+12, Time: 618

使用 12 个任务:

# ./timing 12
Result: 1.0011e+12, Time: 788
Result: 1.0011e+12, Time: 609
Result: 1.0011e+12, Time: 812
Result: 1.0011e+12, Time: 605
Result: 1.0011e+12, Time: 808
Result: 1.0011e+12, Time: 1050
Result: 1.0011e+12, Time: 817
Result: 1.0011e+12, Time: 830
Result: 1.0011e+12, Time: 597
Result: 1.0011e+12, Time: 573
Result: 1.0011e+12, Time: 586
Result: 1.0011e+12, Time: 583

使用 24 个任务:

# ./timing 24
Result: 1.0011e+12, Time: 762
Result: 1.0011e+12, Time: 1033
Result: 1.0011e+12, Time: 735
Result: 1.0011e+12, Time: 1051
Result: 1.0011e+12, Time: 1060
Result: 1.0011e+12, Time: 757
Result: 1.0011e+12, Time: 1075
Result: 1.0011e+12, Time: 758
Result: 1.0011e+12, Time: 745
Result: 1.0011e+12, Time: 1165
Result: 1.0011e+12, Time: 1032
Result: 1.0011e+12, Time: 1160
Result: 1.0011e+12, Time: 757
Result: 1.0011e+12, Time: 743
Result: 1.0011e+12, Time: 736
Result: 1.0011e+12, Time: 1028
Result: 1.0011e+12, Time: 1109
Result: 1.0011e+12, Time: 1018
Result: 1.0011e+12, Time: 1338
Result: 1.0011e+12, Time: 743
Result: 1.0011e+12, Time: 1061
Result: 1.0011e+12, Time: 1046
Result: 1.0011e+12, Time: 1341
Result: 1.0011e+12, Time: 761

使用 48 个任务:

# ./timing 48
Result: 1.0011e+12, Time: 1591
Result: 1.0011e+12, Time: 1776
Result: 1.0011e+12, Time: 1923
Result: 1.0011e+12, Time: 1876
Result: 1.0011e+12, Time: 2002
Result: 1.0011e+12, Time: 1649
Result: 1.0011e+12, Time: 1955
Result: 1.0011e+12, Time: 1728
Result: 1.0011e+12, Time: 1632
Result: 1.0011e+12, Time: 1418
Result: 1.0011e+12, Time: 1904
Result: 1.0011e+12, Time: 1847
Result: 1.0011e+12, Time: 1595
Result: 1.0011e+12, Time: 1910
Result: 1.0011e+12, Time: 1530
Result: 1.0011e+12, Time: 1824
Result: 1.0011e+12, Time: 1588
Result: 1.0011e+12, Time: 1656
Result: 1.0011e+12, Time: 1876
Result: 1.0011e+12, Time: 1683
Result: 1.0011e+12, Time: 1403
Result: 1.0011e+12, Time: 1730
Result: 1.0011e+12, Time: 1476
Result: 1.0011e+12, Time: 1938
Result: 1.0011e+12, Time: 1429
Result: 1.0011e+12, Time: 1888
Result: 1.0011e+12, Time: 1530
Result: 1.0011e+12, Time: 1754
Result: 1.0011e+12, Time: 1794
Result: 1.0011e+12, Time: 1935
Result: 1.0011e+12, Time: 1757
Result: 1.0011e+12, Time: 1572
Result: 1.0011e+12, Time: 1474
Result: 1.0011e+12, Time: 1609
Result: 1.0011e+12, Time: 1394
Result: 1.0011e+12, Time: 1655
Result: 1.0011e+12, Time: 1480
Result: 1.0011e+12, Time: 2061
Result: 1.0011e+12, Time: 2056
Result: 1.0011e+12, Time: 1598
Result: 1.0011e+12, Time: 1630
Result: 1.0011e+12, Time: 1623
Result: 1.0011e+12, Time: 2073
Result: 1.0011e+12, Time: 1395
Result: 1.0011e+12, Time: 1487
Result: 1.0011e+12, Time: 1854
Result: 1.0011e+12, Time: 1569
Result: 1.0011e+12, Time: 1530

这段代码有问题吗?矢量化是parallel_for的问题吗?我可以使用 perf 或类似工具获得更好的见解吗?

超线程的存在是因为线程(在现实世界中(经常必须等待内存中的数据,在数据传输过程中使物理内核基本上处于空闲状态。您的示例(以及 CPU,例如通过预取(正在努力避免这种内存限制,因此通过饱和线程数,同一内核上的任何两个超线程都在竞争其执行端口。请注意,CPU 上每个内核周期只有 3 个整数向量 ALU 可用 - 调度程序可能会让它们都忙于一个线程的操作。

使用 1 个线程或 12 个线程,您不会真正遇到此争用。对于 24 个线程,只有当每个线程都调度到其自己的物理内核时,您才能避免此问题,这可能不会发生(因此您开始看到更糟糕的计时(。使用48个内核,您肯定会遇到上述问题。

正如Harold所提到的,您可能还会受到存储限制(这是超线程对竞争的另一个资源(。

您可能需要英特尔 VTune 来证明这一点,但我猜这是因为工作线程在加载和存储之间没有做很多计算工作,它们反而受到 CPU 从 RAM 加载数据的速度的限制。因此,您拥有的线程越多,它们就越争用并相互缺乏有限的内存带宽。正如英特尔的《检测线程应用程序中的内存带宽饱和》文档所述:

随着越来越多的线程或进程共享缓存容量和内存带宽的有限资源,线程应用程序的可伸缩性可能会受到限制。随着更多线程的引入,内存密集型线程应用程序可能会受到内存带宽饱和的影响。在这种情况下,线程应用程序将无法按预期缩放,并且性能可能会降低。 ...任何并行应用程序的带宽饱和的明显症状都是不缩放行为。

使用 VTune 等工具进行性能分析是确定瓶颈所在的唯一方法。VTune 的专长在于它可以分析 CPU 硬件级别的性能,并且作为英特尔工具,它可以访问其他工具可能无法访问的性能计数器和见解,因此在 CPU 看到瓶颈时揭示瓶颈。对于AMD CPU,等效工具是CodeXL。可能使用的其他工具包括性能计数器监视器(从 https://stackoverflow.com/a/4015983 开始(,如果运行Windows,Visual Studio的CPU分析器(来自 https://stackoverflow.com/a/3489965(。

要分析指令级别的性能瓶颈,英特尔架构代码分析器可能会有用。它是一个静态分析器,可对给定英特尔架构的吞吐量、延迟和数据依赖关系进行理论分析。但是,估计值不包括内存、缓存等的影响。有关更多信息,请参阅什么是 IACA 以及如何使用它?。

最新更新