当前位置: 首页 > 知识库问答 >
问题:

当内存顺序放松时,C延迟会增加

鲍驰
2023-03-14

我正在Windows 7 64位VS2013(x64版本)上尝试内存排序。我想使用最快的同步来共享对容器的访问。我选择了原子比较和交换。

我的程序生成两个线程。写入程序推送一个向量,读取器会检测到这一点。

起初,我没有指定任何内存顺序,所以我假设它使用的是memory\u order\u seq\u cst?

使用memory\u order\u seq\u cst时,每个操作的延迟为340-380个周期。

为了尝试提高性能,我让存储使用内存\u顺序\u释放,而加载使用内存\u顺序\u获取。

然而,每次操作的延迟增加到大约1940个周期。

我误解了什么吗?下面的完整代码。

使用默认的内存顺序cst:

#include <iostream>
#include <atomic>
#include <thread>
#include <vector>

std::atomic<bool> _lock{ false };
std::vector<uint64_t> _vec;
std::atomic<uint64_t> _total{ 0 };
std::atomic<uint64_t> _counter{ 0 };
static const uint64_t LIMIT = 1000000;

void writer()
{
    while (_counter < LIMIT)
    {
        bool expected{ false };
        bool val = true;

        if (_lock.compare_exchange_weak(expected, val))
        {
            _vec.push_back(__rdtsc());
            _lock = false;
        }
    }
}

void reader()
{
    while (_counter < LIMIT)
    {
        bool expected{ false };
        bool val = true;

        if (_lock.compare_exchange_weak(expected, val))
        {
            if (_vec.empty() == false)
            {
                const uint64_t latency = __rdtsc() - _vec[0];
                _total += (latency);
                ++_counter;
                _vec.clear();
            }

            _lock = false;
        }
    }
}

int main()
{
    std::thread t1(writer);
    std::thread t2(reader);

    t2.detach();
    t1.join();

    std::cout << _total / _counter << " cycles per op" << std::endl;
}

使用“memory\u order\u acquire”(获取)和“memory\u order\u release”(释放):

void writer()
{
    while (_counter < LIMIT)
    {
        bool expected{ false };
        bool val = true;

        if (_lock.compare_exchange_weak(expected, val, std::memory_order_acquire))
        {
            _vec.push_back(__rdtsc());
            _lock.store(false, std::memory_order_release);
        }
    }
}

void reader()
{
    while (_counter < LIMIT)
    {
        bool expected{ false };
        bool val = true;

        if (_lock.compare_exchange_weak(expected, val, std::memory_order_acquire))
        {
            if (_vec.empty() == false)
            {
                const uint64_t latency = __rdtsc() - _vec[0];
                _total += (latency);
                ++_counter;
                _vec.clear();
            }

            _lock.store(false, std::memory_order_release);
        }
    }
}

共有1个答案

徐新荣
2023-03-14
匿名用户

您没有任何保护措施来防止线程在释放锁后立即再次获取锁,结果却找到了vec。empty()不是false,或者存储另一个TSC值,覆盖了读卡器从未看到的TSC值。我怀疑您的更改会让读者浪费更多的时间来阻止编写器(反之亦然),从而导致实际吞吐量降低。

TL:DR:真正的问题是您的锁定缺乏公平性(对于刚刚解锁的线程来说,再次锁定它太容易了,无法成为赢得比赛的线程),以及您使用该锁的方式。(您必须在确定是否有任何有用的事情要做之前接受它,迫使其他线程重试,并导致内核之间缓存行的额外传输。)

让一个线程在另一个线程没有轮到的情况下重新获取锁总是无用和浪费的工作,这与许多实际情况不同,在实际情况下需要更多的重复才能填满或清空队列。这是一个糟糕的生产者-消费者算法(队列太小(大小为1),和/或读取器在读取vec[0]后丢弃所有向量元素),也是最糟糕的锁定方案。

<代码>\u锁定。存储(false,seq\u cst) 编译为xchg,而不是普通的mov存储。它必须等待存储缓冲区耗尽,速度非常慢(例如,在Skylake上,微码化为8个UOP,在L1d缓存中已经很热的无争用情况下,对于许多重复的背靠背操作,吞吐量为每23个周期一个。您没有指定任何硬件)。

_lock.store(false, std::memory_order_release);确实只编译到一个普通的mov存储,没有额外的障碍指令。所以_counter的重新加载可以与它并行进行(尽管分支预测推测执行使这成为一个问题)。更重要的是,下一次CAS尝试获取锁实际上可以更快地尝试。

当多个内核在缓存线上运行时,可能会有硬件仲裁来访问缓存线,这可能是通过一些公平启发法实现的,但我不知道细节是否已知。

脚注1:在最近的一些CPU上,尤其是Skylake派生的CPU上,xchg不如movmford慢。它是在x86上实现seq_cst纯存储的最佳方式。但它比普通的mov慢。

Writer等待false,然后在完成后存储true。读卡器则相反。因此,如果另一条线程没有转弯,作者就永远无法重新进入关键部分。(当您“等待值”时,请使用加载而不是CAS进行只读操作。x86上的CAS需要缓存线的独占所有权,以防止其他线程读取。由于只有一个读卡器和一个写卡器,您不需要任何原子RMW来工作。)

如果有多个读卡器和多个写卡器,则可以使用一个4状态同步变量,其中写卡器尝试将其从0 CAS到1,然后在完成后存储2。读卡器尝试从2 CAS到3,完成后存储0。

SPSC(单生产者单消费者)案例很简单:

enum lockstates { LK_WRITER=0, LK_READER=1, LK_EXIT=2 };
std::atomic<lockstates> shared_lock;
uint64_t shared_queue;  // single entry

uint64_t global_total{ 0 }, global_counter{ 0 };
static const uint64_t LIMIT = 1000000;

void writer()
{
    while(1) {
        enum lockstates lk;
        while ((lk = shared_lock.load(std::memory_order_acquire)) != LK_WRITER) {
                if (lk == LK_EXIT) 
                        return;
                else
                        SPIN;     // _mm_pause() or empty
        }

        //_vec.push_back(__rdtsc());
        shared_queue = __rdtsc();
        shared_lock.store(LK_READER, ORDER);   // seq_cst or release
    }
}

void reader()
{
    uint64_t total=0, counter=0;
    while(1) {
        enum lockstates lk;
        while ((lk = shared_lock.load(std::memory_order_acquire)) != LK_READER) {
                SPIN;       // _mm_pause() or empty
        }

        const uint64_t latency = __rdtsc() - shared_queue;  // _vec[0];
        //_vec.clear();
        total += latency;
        ++counter;
        if (counter < LIMIT) {
                shared_lock.store(LK_WRITER, ORDER);
        }else{
                break;  // must avoid storing a LK_WRITER right before LK_EXIT, otherwise writer races and can overwrite with LK_READER
        }
    }
    global_total = total;
    global_counter = counter;
    shared_lock.store(LK_EXIT, ORDER);
}

戈德博尔特上的完整版本。在我的i7-6700kSkylake桌面上(2核turbo=4200MHz,TSC=4008MHz),用clang 9.0.1-O3编译。正如预期的那样,数据非常嘈杂;我做了一系列运行并手动选择了一个低点和高点,忽略了一些可能由于热身效果而导致的真正异常高点。

在单独的物理核心上:

  • -DSPIN='_mm_pause()'-DORDER=std::memory_order_release:~180~~210个周期/op,基本上为零machine_clears.memory_ordering(如19总计超过1000000个op,这要归功于自旋等待循环中的暂停。)
  • -DSPIN='_mm_pause()'-DORDER=std::memory_order_seq_cst:~195到~215 ref周期/op,相同的近零机器清除。
  • -DSPIN="-DORDER=std::memory_order_release:~195~~225 ref c/op,9~10 M/sec机器在没有暂停的情况下清除。
  • -DSPIN="-DORDER=std::memory_order_seq_cst:更多的变量和更慢,~250~~315 c/op,810 M/sec机器清除没有暂停

这些计时比我系统上的seq_cst“快速”原始时间快3倍。使用std::向量

从最佳版本(mo\U版本,旋转以避免机器清除):

$ clang++ -Wall -g -DSPIN='_mm_pause()' -DORDER=std::memory_order_release -O3 inter-thread.cpp -pthread && 
 perf stat --all-user -etask-clock:u,context-switches,cpu-migrations,page-faults,cycles:u,branches:u,instructions:u,uops_issued.any:u,uops_executed.thread:u,machine_clears.memory_ordering -r4 ./a.out
195 ref cycles per op. total ticks: 195973463 / 1000000 ops
189 ref cycles per op. total ticks: 189439761 / 1000000 ops
193 ref cycles per op. total ticks: 193271479 / 1000000 ops
198 ref cycles per op. total ticks: 198413469 / 1000000 ops

 Performance counter stats for './a.out' (4 runs):

            199.83 msec task-clock:u              #    1.985 CPUs utilized            ( +-  1.23% )
                 0      context-switches          #    0.000 K/sec                  
                 0      cpu-migrations            #    0.000 K/sec                  
               128      page-faults               #    0.643 K/sec                    ( +-  0.39% )
       825,876,682      cycles:u                  #    4.133 GHz                      ( +-  1.26% )
        10,680,088      branches:u                #   53.445 M/sec                    ( +-  0.66% )
        44,754,875      instructions:u            #    0.05  insn per cycle           ( +-  0.54% )
       106,208,704      uops_issued.any:u         #  531.491 M/sec                    ( +-  1.07% )
        78,593,440      uops_executed.thread:u    #  393.298 M/sec                    ( +-  0.60% )
                19      machine_clears.memory_ordering #    0.094 K/sec                    ( +-  3.36% )

           0.10067 +- 0.00123 seconds time elapsed  ( +-  1.22% )

从最差的版本来看(mo\u seq\u cst,nopause):自旋等待循环旋转得更快,因此发出/执行的分支和UOP要高得多,但实际有用的吞吐量稍差。

$ clang++ -Wall -g -DSPIN='' -DORDER=std::memory_order_seq_cst -O3 inter-thread.cpp -pthread && 
 perf stat --all-user -etask-clock:u,context-switches,cpu-migrations,page-faults,cycles:u,branches:u,instructions:u,uops_issued.any:u,uops_executed.thread:u,machine_clears.memory_ordering -r4 ./a.out
280 ref cycles per op. total ticks: 280529403 / 1000000 ops
215 ref cycles per op. total ticks: 215763699 / 1000000 ops
282 ref cycles per op. total ticks: 282170615 / 1000000 ops
174 ref cycles per op. total ticks: 174261685 / 1000000 ops

 Performance counter stats for './a.out' (4 runs):

            207.82 msec task-clock:u              #    1.985 CPUs utilized            ( +-  4.42% )
                 0      context-switches          #    0.000 K/sec                  
                 0      cpu-migrations            #    0.000 K/sec                  
               130      page-faults               #    0.623 K/sec                    ( +-  0.67% )
       857,989,286      cycles:u                  #    4.129 GHz                      ( +-  4.57% )
       236,364,970      branches:u                # 1137.362 M/sec                    ( +-  2.50% )
       630,960,629      instructions:u            #    0.74  insn per cycle           ( +-  2.75% )
       812,986,840      uops_issued.any:u         # 3912.003 M/sec                    ( +-  5.98% )
       637,070,771      uops_executed.thread:u    # 3065.514 M/sec                    ( +-  4.51% )
         1,565,106      machine_clears.memory_ordering #    7.531 M/sec                    ( +- 20.07% )

           0.10468 +- 0.00459 seconds time elapsed  ( +-  4.38% )

将读取器和写入器固定在一个物理内核的逻辑内核上会大大加快速度:在我的系统上,内核3和7是兄弟姐妹,所以Linuxtaskset-c 3,7。/a.out阻止内核在其他任何地方调度它们:每个操作33到39个ref周期,或者80到82个没有暂停

(使用HT在一个内核上执行的线程之间的数据交换将使用什么?,)

$ clang++ -Wall -g -DSPIN='_mm_pause()' -DORDER=std::memory_order_release -O3 inter-thread.cpp -pthread && 
 taskset -c 3,7 perf stat --all-user -etask-clock:u,context-switches,cpu-migrations,page-faults,cycles:u,branches:u,instructions:u,uops_issued.any:u,uops_executed.thread:u,machine_clears.memory_ordering -r4 ./a.out
39 ref cycles per op. total ticks: 39085983 / 1000000 ops
37 ref cycles per op. total ticks: 37279590 / 1000000 ops
36 ref cycles per op. total ticks: 36663809 / 1000000 ops
33 ref cycles per op. total ticks: 33546524 / 1000000 ops

 Performance counter stats for './a.out' (4 runs):

             89.10 msec task-clock:u              #    1.942 CPUs utilized            ( +-  1.77% )
                 0      context-switches          #    0.000 K/sec                  
                 0      cpu-migrations            #    0.000 K/sec                  
               128      page-faults               #    0.001 M/sec                    ( +-  0.45% )
       365,711,339      cycles:u                  #    4.104 GHz                      ( +-  1.66% )
         7,658,957      branches:u                #   85.958 M/sec                    ( +-  0.67% )
        34,693,352      instructions:u            #    0.09  insn per cycle           ( +-  0.53% )
        84,261,390      uops_issued.any:u         #  945.680 M/sec                    ( +-  0.45% )
        71,114,444      uops_executed.thread:u    #  798.130 M/sec                    ( +-  0.16% )
                16      machine_clears.memory_ordering #    0.182 K/sec                    ( +-  1.54% )

           0.04589 +- 0.00138 seconds time elapsed  ( +-  3.01% )

在共享相同物理核的逻辑核上。最佳情况下,延迟比内核之间的延迟低5倍,这也是暂停mo\U释放的最佳情况。但实际的基准测试只在40%的时间内完成,而不是20%

  • -DSPIN=“\u mm\u pause()”-DORDER=std::memory\u order\u release:~ 33到39个参考周期/操作,接近零。机器清除。内存\u排序

所有这些测试都是使用clang进行的,它使用xchg进行seq\u cst存储<代码>g使用movmfence,在暂停情况下速度较慢,在没有暂停的情况下速度更快,机器清除次数更少。(对于超线程情况。)通常情况下,带暂停的独立cores情况非常相似,但在不带暂停的独立cores seq\u cst情况下速度更快。(同样,特别是在Skylake上,针对这一测试。)

同样值得检查perf计数器的machine_clears.memory_ordering(为什么要为其他逻辑处理器导致的内存顺序违规刷新管道?)。

我确实检查了我的Skylake i7-6700k,在4.2GHz时,每秒machine_clears.memory_ordering的速率没有显著差异(对于快速seq_cst和慢速发布,大约5M/秒)。seq_cst版本(400到422)的“每次操作循环”结果出人意料地一致。我的CPU的TSC参考频率是4008MHz,最大涡轮增压时的实际核心频率是4200MHz。如果你有340-380周期,我假设你的CPU的最大涡轮增压相对于其参考频率比我的更高。和/或不同的微架构。

但我发现mo\u版本的结果差异很大:在Arch GNU/Linux上,GCC9.3.0-O3版本:一次运行5790次,另一次运行2269次。对于两次运行的clang9.0.1<代码>-O3<代码>73346和7333,是的,实际上是一个系数10)。这是一个惊喜。在清空/推送向量时,两个版本都没有进行释放/分配内存的系统调用,我也没有看到很多内存排序机器从叮当的版本中清除。根据您最初的极限,两次使用叮当的跑步显示每次操作1394和22101个周期。

使用clang时,即使是seq\u cst时间的变化也比使用GCC时大一些,并且更高,如630到700。(对于seq\u cst纯存储,g使用mov,clang使用xchg,就像MSVC一样)。

其他带有mo_release的perf计数器显示出每秒类似的指令、分支和uops速率,所以我认为这表明代码只是花了更多时间在关键部分用错误的线程旋转轮子,而另一个陷入重试。

两次性能运行,第一次是mo\U release,第二次是mo\U seq\u cst。

$ clang++ -DORDER=std::memory_order_release -O3 inter-thread.cpp -pthread &&
 perf stat --all-user -etask-clock:u,context-switches,cpu-migrations,page-faults,cycles:u,branches:u,instructions:u,uops_issued.any:u,uops_executed.thread:u,machine_clears.memory_ordering -r1 ./a.out
27989 cycles per op

 Performance counter stats for './a.out':

         16,350.66 msec task-clock:u              #    2.000 CPUs utilized          
                 0      context-switches          #    0.000 K/sec                  
                 0      cpu-migrations            #    0.000 K/sec                  
               231      page-faults               #    0.014 K/sec                  
    67,412,606,699      cycles:u                  #    4.123 GHz                    
       697,024,141      branches:u                #   42.630 M/sec                  
     3,090,238,185      instructions:u            #    0.05  insn per cycle         
    35,317,247,745      uops_issued.any:u         # 2159.989 M/sec                  
    17,580,390,316      uops_executed.thread:u    # 1075.210 M/sec                  
       125,365,500      machine_clears.memory_ordering #    7.667 M/sec                  

       8.176141807 seconds time elapsed

      16.342571000 seconds user
       0.000000000 seconds sys


$ clang++ -DORDER=std::memory_order_seq_cst -O3 inter-thread.cpp -pthread &&
 perf stat --all-user -etask-clock:u,context-switches,cpu-migrations,page-faults,cycles:u,branches:u,instructions:u,uops_issued.any:u,uops_executed.thread:u,machine_clears.memory_ordering -r1 ./a.out
779 cycles per op

 Performance counter stats for './a.out':

            875.59 msec task-clock:u              #    1.996 CPUs utilized          
                 0      context-switches          #    0.000 K/sec                  
                 0      cpu-migrations            #    0.000 K/sec                  
               137      page-faults               #    0.156 K/sec                  
     3,619,660,607      cycles:u                  #    4.134 GHz                    
        28,100,896      branches:u                #   32.094 M/sec                  
       114,893,965      instructions:u            #    0.03  insn per cycle         
     1,956,774,777      uops_issued.any:u         # 2234.806 M/sec                  
     1,030,510,882      uops_executed.thread:u    # 1176.932 M/sec                  
         8,869,793      machine_clears.memory_ordering #   10.130 M/sec                  

       0.438589812 seconds time elapsed

       0.875432000 seconds user
       0.000000000 seconds sys

我将内存顺序修改为CPP宏,这样您就可以使用-DORDER=std::memory\u order\u release编译代码,以获得慢速版本
<代码>获取
顺序cst在这里并不重要;它在x86上编译为相同的asm,用于加载和原子RMW。只有纯门店需要seq\u cst的特殊asm。

您还遗漏了stdint。h和intrin。h(MSVC)/x86intrin。h(其他所有内容)。固定版本位于带clang和MSVC的Godbolt上。早些时候,我将限制提高了10倍,以确保CPU频率有时间在大部分时间段内提升到最大turbo,但恢复了该更改,因此测试mo\U释放只需几秒钟,而不是几分钟。

设置检查特定总TSC周期的限制可能有助于它在更一致的时间内退出。这还不算写作者被关在门外的时间,但总的来说,运行时间应该非常长,不太可能。

如果您只是想测量线程间延迟,那么也会有很多非常复杂的事情发生。

(CPU之间的通信是如何发生的?)

您的两个线程都在读取写入程序每次更新的总数,而不是在完成后只存储一个标志。因此,写入程序具有潜在的内存排序机制,可以从读取另一个线程写入的变量中清除。

在读卡器中还有一个原子RMW增量,即计数器,即使该变量是读卡器专用的。它可以是一个普通的非原子全局,您可以在读取器之后阅读。join(),或者更好的是,它可以是一个局部变量,您只能在循环后存储到全局变量。(由于发布存储,一个普通的非原子全局可能仍然会在每次迭代中存储到内存中,而不是保存在寄存器中。由于这是一个很小的程序,所有全局可能都相邻,并且可能在同一缓存线中。)

std::向量也是不必要的。__rdtsc()不会为零,除非它环绕64位计数器1,因此您可以使用0作为标量uint64_t中的哨兵值表示空。或者如果您修复了锁定,因此阅读器无法在没有编写器的情况下重新进入关键部分,您可以删除该检查。

脚注2:对于~ 4GHz TSC参考频率,即2^64/10^9秒,接近于2^32秒~=136年,足以覆盖TSC。注意,TSC参考频率不是当前核心时钟频率;对于给定的CPU,它固定为某个值。通常接近额定“贴纸”频率,而不是最大涡轮。

此外,在ISO C中,带前导的名称保留在全局范围内。不要将它们用于自己的变量。(通常不在任何地方。如果确实需要,可以使用尾随下划线。)

 类似资料:
  • 我最近开始学习C++,以前我是用围棋编程的。 我最近被告知不应该使用,因为抛出的异常可能会导致分配的内存不是D并导致内存泄漏。一个流行的解决方案是RAII,我找到了一个很好的解释为什么要使用RAII以及它在这里是什么。 然而,从Go开始,整个RAII的事情似乎是不必要的复杂。Go有一个叫做defer的东西,它以一种非常直观的方式解决了这个问题。您只需将作用域结束时要执行的操作包装在中,例如或,它将

  • > 指向自动分配实例的指针是否可以使该实例即使在实例化的作用域被保留后也不被解除分配? <罢工> 在 我读到的这篇文章说所有指向去分配内存的指针都是无效的。 但这家伙说的是手动解锁还是自动解锁后的行为? 这是一个示例:

  • 问题内容: 因此,我将顺序的ajax链接在一起,以按顺序加载URL数组。最初,我使用代替,并且无论哪种方式都可以正常工作- 只要所有网址都存在。但是,由于可能会丢失文件,因此我想对此进行补偿,然后,最后通知用户丢失了哪些文件,以便更轻松地进行纠正。 但是,问题是,在丢失的文件/ 404上,代码会像应执行的那样执行,但随后退出循环,从而阻止了进一步的ajax调用。因此,我认为,我需要某种方式来处理并

  • 问题内容: 我想知道一段时间后如何调用函数。我已经尝试过time.sleep(),但是这会暂停整个脚本。我希望脚本继续进行,但是??? secs之后调用一个函数并同时运行其他脚本 问题答案: 看一看。它在新线程中运行您的函数。

  • 我们有一个应用程序引擎应用程序,它将许多相对较大的文件写入谷歌云商店。这些文件是动态创建的CSV,因此我们使用Python的作为写入该缓冲区的接口。 通常,我们的流程如下所示: 据我们所知,不需要自己关闭。相反,只需要关闭上面的和。 我们在appengine的任务队列调用的

  • 我正在为画廊老板建立一个网站,每个网页都有很多图片。因此,我想延迟加载网页上的图像,使最初的负载较轻。不过,我想以“逐步加强”的方式来实施这一点。 我发现了很多延迟加载方法,但它们都需要修改html代码,这样关闭javascript后网页就没用了。(例如,标记的属性保持未设置,直到图像延迟加载)。 要逐步实现懒惰加载方法,我认为需要以下内容: 阻止浏览器获取图像,即使页面上有其他图像,但仅在启用j