繁体   English   中英

具有原子的C ++ 11 std线程求和非常慢

[英]C++ 11 std thread sumation with atomic very slow

我想学习在VS2012中使用C ++ 11 std :: threads,我编写了一个非常简单的C ++控制台程序,其中包含两个线程,它们仅增加了一个计数器。 我还想测试使用两个线程时的性能差异。 测试程序如下:

#include <iostream>
#include <thread>
#include <conio.h>
#include <atomic>

std::atomic<long long> sum(0);
//long long sum;

using namespace std;

const int RANGE = 100000000;

void test_without_threds()
{
    sum = 0;
    for(unsigned int j = 0; j < 2; j++)
    for(unsigned int k = 0; k < RANGE; k++)
        sum ++ ;
}

void call_from_thread(int tid) 
{
    for(unsigned int k = 0; k < RANGE; k++)
        sum ++ ;
}

void test_with_2_threds()
{
    std::thread t[2];
    sum = 0;
    //Launch a group of threads
    for (int i = 0; i < 2; ++i) {
        t[i] = std::thread(call_from_thread, i);
    }

    //Join the threads with the main thread
    for (int i = 0; i < 2; ++i) {
        t[i].join();
    }
}

int _tmain(int argc, _TCHAR* argv[])
{
    chrono::time_point<chrono::system_clock> start, end;

    cout << "-----------------------------------------\n";
    cout << "test without threds()\n";

    start = chrono::system_clock::now();
    test_without_threds();
    end = chrono::system_clock::now();

    chrono::duration<double> elapsed_seconds = end-start;

    cout << "finished calculation for "
              << chrono::duration_cast<std::chrono::milliseconds>(end - start).count()
              << "ms.\n";

    cout << "sum:\t" << sum << "\n";\

    cout << "-----------------------------------------\n";
    cout << "test with 2_threds\n";

    start = chrono::system_clock::now();
    test_with_2_threds();
    end = chrono::system_clock::now();

    cout << "finished calculation for "
              << chrono::duration_cast<std::chrono::milliseconds>(end - start).count()
              << "ms.\n";

    cout << "sum:\t" << sum << "\n";\

    _getch();
    return 0;
}

现在,当我只使用long long变量(带有注释)作为计数器时,我得到的值与正确的值不同-100000000而不是200000000。我不确定为什么会这样,并且我想两个线程正在更改同时计数器,但我不确定它是如何发生的,因为++只是一个非常简单的指令。 似乎线程在开始时就缓存sum变量。 两个线程的性能为110毫秒,而一个线程的性能为200毫秒。

因此,根据文档的正确方法是使用std :: atomic。 但是,现在这两种情况的性能都差得多,因为没有线程大约3300 ms,带有线程大约15820 ms。 在这种情况下,使用std :: atomic的正确方法是什么?

我不确定为什么会这样,我想两个线程会同时更改计数器,但是我不确定它是如何发生的,因为++只是一个非常简单的指令。

每个线程都将sum的值拉入一个寄存器,递增该寄存器,最后在循环结束时将其写回到内存中。

因此,根据文档的正确方法是使用std :: atomic。 但是,现在这两种情况的性能都差得多,因为没有线程大约3300 ms,带有线程大约15820 ms。 在这种情况下,使用std :: atomic的正确方法是什么?

您需要为std::atomic提供的同步服务付费。 尽管您可以通过优化add的内存顺序来提高性能,但是它的速度几乎不如使用非同步整数。

sum.fetch_add(1, std::memory_order_relaxed);

在这种情况下,您将为x86进行编译并在64位整数上运行。 这意味着编译器必须生成代码以通过两个32位操作来更新该值。 如果将目标平台更改为x64,则编译器将生成代码以单个64位操作进行增量。

通常,解决此类问题的方法是减少对共享数据的写入次数。

您的代码有两个问题。 首先,所有涉及的“输入”都是编译时常量,因此好的编译器可以预先计算单线程代码的值,因此(无论您为range给出的值如何)它都显示为在0中运行女士。

其次,您要在所有线程之间共享一个变量( sum ),从而迫使所有线程的访问在那时都被同步。 如果没有同步,则会产生未定义的行为。 正如您已经发现的那样,同步对该变量的访问非常昂贵,因此通常在完全合理的情况下都希望避免使用它。

一种方法是对每个线程使用单独的小计,以便它们都可以并行进行加法,而无需同步,最后将各个结果加在一起。

另一点是确保防止虚假共享。 当两个(或多个)线程正在写入实际上是独立的但已分配在同一高速缓存行中的数据时,就会出现错误共享。 在这种情况下,即使没有线程之间实际共享的任何数据,也可以序列化对内存的访问。

基于这些因素,我已经稍微重写了您的代码,以为每个线程创建一个单独的sum变量。 这些变量是一个class类型,可以(公平地)直接访问数据,但是确实阻止了优化器看到它可以在编译时完成整个计算,因此我们最终将一个线程与4进行了比较(这提醒我:由于我使用的是四核计算机,所以确实将线程数从2增加到4。 我将数字移到const变量中,因此使用不同数量的线程进行测试应该很容易。

#include <iostream>
#include <thread>
#include <conio.h>
#include <atomic>
#include <numeric>

const int num_threads = 4;

struct val {
    long long sum;
    int pad[2];

    val &operator=(long long i) { sum = i; return *this; }
    operator long long &() { return sum; }
    operator long long() const { return sum; }
};

val sum[num_threads];

using namespace std;

const int RANGE = 100000000;

void test_without_threds()
{
    sum[0] = 0LL;
    for(unsigned int j = 0; j < num_threads; j++)
    for(unsigned int k = 0; k < RANGE; k++)
        sum[0] ++ ;
}

void call_from_thread(int tid) 
{
    for(unsigned int k = 0; k < RANGE; k++)
        sum[tid] ++ ;
}

void test_with_threads()
{
    std::thread t[num_threads];
    std::fill_n(sum, num_threads, 0);
    //Launch a group of threads
    for (int i = 0; i < num_threads; ++i) {
        t[i] = std::thread(call_from_thread, i);
    }

    //Join the threads with the main thread
    for (int i = 0; i < num_threads; ++i) {
        t[i].join();
    }
    long long total = std::accumulate(std::begin(sum), std::end(sum), 0LL);
}

int main()
{
    chrono::time_point<chrono::system_clock> start, end;

    cout << "-----------------------------------------\n";
    cout << "test without threds()\n";

    start = chrono::system_clock::now();
    test_without_threds();
    end = chrono::system_clock::now();

    chrono::duration<double> elapsed_seconds = end-start;

    cout << "finished calculation for "
              << chrono::duration_cast<std::chrono::milliseconds>(end - start).count()
              << "ms.\n";

    cout << "sum:\t" << sum << "\n";\

    cout << "-----------------------------------------\n";
    cout << "test with threads\n";

    start = chrono::system_clock::now();
    test_with_threads();
    end = chrono::system_clock::now();

    cout << "finished calculation for "
              << chrono::duration_cast<std::chrono::milliseconds>(end - start).count()
              << "ms.\n";

    cout << "sum:\t" << sum << "\n";\

    _getch();
    return 0;
}

当我运行此命令时,我的结果更接近于我希望您希望的结果:

-----------------------------------------
test without threds()
finished calculation for 78ms.
sum:    000000013FCBC370
-----------------------------------------
test with threads
finished calculation for 15ms.
sum:    000000013FCBC370

...的总和是相同的,但是N个线程将速度提高了大约N倍(取决于可用内核的数量)。

尝试使用前缀增量,这将提高性能。 在我的机器上测试,std :: memory_order_relaxed没有任何优势。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM