简单的C ++循环无法从多线程中受益

时间:2019-08-27 07:03:06

标签: c++ multithreading

我有一些非常简单的C ++代码,可以肯定的是,使用多线程可以使运行速度提高3倍,但是在Windows 10上,它们在GCC和MSVC上的运行速度仅快3%(或更少)。

没有没有互斥锁和没有共享资源。而且我看不到错误共享或缓存颠簸的影响如何发挥作用,因为每个线程仅修改数组的不同部分,该部分的值超过十亿int。我意识到类似这样的问题很多,但我还没有发现任何可以解决这个特殊谜团的问题。

一个提示可能是,将数组初始化移到add()函数 的循环中确实使多线程与单线程(〜885ms与〜2650ms)相比,该函数快了3倍。

请注意,add()函数仅被计时,在我的计算机上大约需要600毫秒。我的机器有4个超线程内核,因此我正在将threadCount设置为8,然后设置为1的情况下运行代码。

有什么想法吗?有没有办法(在适当的时候)关闭处理器中的功能,这些功能会导致诸如错误共享(可能就像我们在这里看到的那样)之类的事情发生?

#include <chrono>
#include <iostream>
#include <thread>

void startTimer();
void stopTimer();
void add(int* x, int* y, int threadIdx);

namespace ch = std::chrono;
auto start = ch::steady_clock::now();
const int threadCount = 8;
int itemCount = 1u << 30u; // ~1B items
int itemsPerThread = itemCount / threadCount;

int main() {
    int* x = new int[itemCount];
    int* y = new int[itemCount];

    // Initialize arrays
    for (int i = 0; i < itemCount; i++) {
        x[i] = 1;
        y[i] = 2;
    }

    // Call add() on multiple threads
    std::thread threads[threadCount];
    startTimer();
    for (int i = 0; i < threadCount; ++i) {
        threads[i] = std::thread(add, x, y, i);
    }
    for (auto& thread : threads) {
        thread.join();
    }
    stopTimer();

    // Verify results
    for (int i = 0; i < itemCount; ++i) {
        if (y[i] != 3) {
            std::cout << "Error!";
        }
    }

    delete[] x;
    delete[] y;
}

void add(int* x, int* y, int threadIdx) {
    int firstIdx = threadIdx * itemsPerThread;
    int lastIdx = firstIdx + itemsPerThread - 1;

    for (int i = firstIdx; i <= lastIdx; ++i) {
        y[i] = x[i] + y[i];
    }
}

void startTimer() {
    start = ch::steady_clock::now();
}

void stopTimer() {
    auto end = ch::steady_clock::now();
    auto duration = ch::duration_cast<ch::milliseconds>(end - start).count();
    std::cout << duration << " ms\n";
}

3 个答案:

答案 0 :(得分:5)

您可能只是在提高计算机的内存传输速率,您正在执行8GB的读取和4GB的写入。

在我的计算机上,您的测试大约需要500毫秒才能完成,即24GB / s(这类似于内存带宽测试仪给出的结果)。

当您通过一次读取和一次写入访问每个内存地址时,缓存就没有太多用处了,因为您没有在重复使用内存。

答案 1 :(得分:5)

您的问题不是处理器。您遇到了RAM读写延迟。由于您的缓存能够保存一些兆字节的数据,因此您远远超出了此存储空间。只要您可以将数据输入到处理器中,多线程就非常有用。与RAM相比,处理器中的缓存非常快。当您超出缓存存储空间时,这将导致RAM延迟测试。

如果您想了解多线程的优势,则必须在缓存大小范围内选择数据大小。


编辑

要做的另一件事是为内核创建更高的工作负载,因此无法识别存储延迟。

边注:请记住,您的核心有几个执行单元。每种运算类型都可以选择一个或多个-整数,浮点数,移位等。这就是说,一个内核每步执行的命令数不能超过一个。特别地,每个执行单元一个操作。您可以保留测试数据的数据大小,并做更多的事情-发挥创意=)仅用整数运算填充队列,这将为您带来多线程的优势。如果您可以在代码中进行更改,在何时何地执行不同的操作,则这也会对加速产生影响。如果想在多线程上获得不错的加速效果,则可以避免使用它。

为避免任何形式的优化,应使用随机测试数据。因此编译器和处理器本身都无法预测操作的结果。

也要避免像if和while这样进行分支。处理器必须预测和执行的每个决定都会减慢您的速度并改变结果。使用分支预测,您将永远无法获得确定性的结果。稍后在“真实”程序中,成为我的客人,做您想做的事。但是,当您要探索多线程世界时,这可能会导致您得出错误的结论。


BTW

请为您使用的每个delete使用一个new,以避免内存泄漏。甚至更好的是,避免使用普通指针newdelete。您应该使用RAII。我建议使用简单的STL容器std::arraystd::vector。这样可以节省大量的调试时间和麻烦。

答案 2 :(得分:4)

并行化的加速受到任务中仍然保持串行状态的部分的限制。这称为Amdahl's law。在您的情况下,初始化阵列需要花费大量的串行时间。

您是否使用-O3编译代码?如果是这样,则编译器可能能够展开和/或向量化某些循环。循环的步幅是可以预测的,因此硬件预取也可能会有所帮助。

您可能还想研究一下使用全部8个超线程是否有用,或者每个内核运行1个线程是否更好(我猜因为问题是内存受限的,所以您可能会受益于全部8个超线程)。

尽管如此,您仍然会受到内存带宽的限制。看一下roofline model。它可以帮助您确定性能以及理论上可以预期的加速。就您而言,您遇到的内存带宽壁垒实际上限制了硬件可实现的操作/秒。