草庐IT

c++ - 为什么 std::fill(0) 比 std::fill(1) 慢?

coder 2023-04-26 原文

我在一个系统上观察到 std::fill在大 std::vector<int>设置常量值 0 时明显且始终较慢与常数值 1 相比或动态值:

5.8 GiB/s 对比 7.5 GiB/s

但是,对于较小的数据大小,结果是不同的,其中 fill(0)是比较快的:



具有多个线程,数据大小为 4 GiB,fill(1)显示更高的斜率,但达到比 fill(0) 低得多的峰值(51 GiB/s 对比 90 GiB/s):



这就提出了第二个问题,为什么fill(1)的峰值带宽?低得多。

对此的测试系统是一个双插槽 Intel Xeon CPU E5-2680 v3,频率设置为 2.5 GHz(通过 /sys/cpufreq)和 8x16 GiB DDR4-2133。我使用 GCC 6.1.0 ( -O3 ) 和 Intel 编译器 17.0.1 ( -fast ) 进行了测试,都得到了相同的结果。 GOMP_CPU_AFFINITY=0,12,1,13,2,14,3,15,4,16,5,17,6,18,7,19,8,20,9,21,10,22,11,23被设置。 Strem/add/24 个线程在系统上获得 85 GiB/s。

我能够在不同的 Haswell 双插槽服务器系统上重现这种效果,但不能在任何其他架构上重现。例如在 Sandy Bridge EP 上,内存性能是相同的,而在缓存中 fill(0)快得多。

这是要重现的代码:

#include <algorithm>
#include <cstdlib>
#include <iostream>
#include <omp.h>
#include <vector>

using value = int;
using vector = std::vector<value>;

constexpr size_t write_size = 8ll * 1024 * 1024 * 1024;
constexpr size_t max_data_size = 4ll * 1024 * 1024 * 1024;

void __attribute__((noinline)) fill0(vector& v) {
    std::fill(v.begin(), v.end(), 0);
}

void __attribute__((noinline)) fill1(vector& v) {
    std::fill(v.begin(), v.end(), 1);
}

void bench(size_t data_size, int nthreads) {
#pragma omp parallel num_threads(nthreads)
    {
        vector v(data_size / (sizeof(value) * nthreads));
        auto repeat = write_size / data_size;
#pragma omp barrier
        auto t0 = omp_get_wtime();
        for (auto r = 0; r < repeat; r++)
            fill0(v);
#pragma omp barrier
        auto t1 = omp_get_wtime();
        for (auto r = 0; r < repeat; r++)
            fill1(v);
#pragma omp barrier
        auto t2 = omp_get_wtime();
#pragma omp master
        std::cout << data_size << ", " << nthreads << ", " << write_size / (t1 - t0) << ", "
                  << write_size / (t2 - t1) << "\n";
    }
}

int main(int argc, const char* argv[]) {
    std::cout << "size,nthreads,fill0,fill1\n";
    for (size_t bytes = 1024; bytes <= max_data_size; bytes *= 2) {
        bench(bytes, 1);
    }
    for (size_t bytes = 1024; bytes <= max_data_size; bytes *= 2) {
        bench(bytes, omp_get_max_threads());
    }
    for (int nthreads = 1; nthreads <= omp_get_max_threads(); nthreads++) {
        bench(max_data_size, nthreads);
    }
}

呈现的结果由 g++ fillbench.cpp -O3 -o fillbench_gcc -fopenmp 编译.

最佳答案

从您的问题 + 从您的答案编译器生成的 asm:

  • fill(0)ERMSB rep stosb 它将在优化的微编码循环中使用 256b 存储。 (如果缓冲区对齐,则效果最佳,可能至少为 32B 或 64B)。
  • fill(1)是一个简单的 128 位 movaps vector 存储循环。无论宽度如何,每个内核时钟周期只能执行一个存储,最高可达 256b AVX。所以128b存储只能填满Haswell的L1D缓存写入带宽的一半。 这就是为什么fill(0)对于高达 ~32kiB 的缓冲区,速度大约是其 2 倍。编译 -march=haswell-march=native解决这个问题 .

    Haswell 只能勉强跟上循环开销,但它仍然可以在每个时钟运行 1 个存储,即使它根本没有展开。但是每个时钟有 4 个融合域 uops,在乱序窗口中占据了大量空间。一些展开可能会让 TLB 未命中在存储发生的位置之前开始解决,因为存储地址 uop 的吞吐量比存储数据的吞吐量要大。对于适合 L1D 的缓冲区,展开可能有助于弥补 ERMSB 和此 vector 循环之间的其余差异。 (对该问题的评论说 -march=native 仅对 L1 有帮助 fill(1)。)

  • 请注意 rep movsd (可用于为 fill(1) 元素实现 int)可能与 rep stosb 执行相同在哈斯韦尔。
    虽然只有官方文档只保证ERMSB给出快速rep stosb (但不是 rep stosd ),actual CPUs that support ERMSB use similarly efficient microcode for rep stosd . IvyBridge 有一些疑问,可能只有 b很快。查看@BeeOnRope 的精彩 ERMSB answer更新。

    gcc 有一些用于字符串操作的 x86 调整选项( like -mstringop-strategy= alg and -mmemset-strategy=strategy ),但 IDK(如果有的话)会让它实际发出 rep movsdfill(1) .可能不是,因为我假设代码以循环开始,而不是 memset .

    With more than one thread, at 4 GiB data size, fill(1) shows a higher slope, but reaches a much lower peak than fill(0) (51 GiB/s vs 90 GiB/s):



    正常 movaps存储到冷缓存行会触发 Read For Ownership (RFO) .当 movaps 时,大量实际 DRAM 带宽用于从内存中读取缓存行。写入前 16 个字节。 ERMSB 存储对其存储使用无 RFO 协议(protocol),因此内存 Controller 仅进行写入。 (除了杂项读取,比如页表,即使在 L3 缓存中也有任何页面遍历未命中,并且可能在中断处理程序中出现一些加载未命中等等)。

    @BeeOnRope explains in comments常规 RFO 存储与 ERMSB 使用的 RFO 避免协议(protocol)之间的差异对服务器 CPU 上的某些缓冲区大小范围存在不利影响,其中非核心/L3 缓存中存在高延迟。 另请参阅链接的 ERMSB 答案,了解有关 RFO 与非 RFO 的更多信息,以及多核 Intel CPU 中非核心(L3/内存)的高延迟是单核带宽的问题。

    movntps ( _mm_stream_ps() ) 商店 是弱排序的,因此它们可以绕过缓存并一次直接进入整个缓存行的内存,而无需将缓存行读入 L1D。 movntps避免 RFO,例如 rep stos做。 ( rep stos 存储可以相互重新排序,但不能超出指令的边界。)

    您的 movntps结果你更新的答案令人惊讶。
    对于具有大缓冲区的单个线程,您的结果是 movnt >> 常规 RFO > ERMSB .因此,这两种非 RFO 方法位于普通旧商店的相反两侧真的很奇怪,而且 ERMSB 远非最佳。我目前没有对此的解释。 (欢迎编辑并提供解释 + 良好的证据)。

    正如我们所料,movnt允许多个线程实现高聚合存储带宽,如 ERMSB。 movnt总是直接进入行填充缓冲区,然后进入内存,因此适合缓存的缓冲区大小要慢得多。每个时钟一个 128b vector 足以轻松地将单个内核的无 RFO 带宽饱和到 DRAM。大概 vmovntps ymm (256b) 仅比 vmovntps xmm 具有可衡量的优势(128b) 在存储受 CPU 限制的 AVX 256b 向量化计算的结果时(即仅当它省去解包到 128b 的麻烦时)。
    movnti带宽很低,因为存储在 4B 块中的瓶颈是每个时钟 1 个存储 uop 将数据添加到行填充缓冲区,而不是将这些行满缓冲区发送到 DRAM(直到您有足够的线程来饱和内存带宽)。

    @osgx 发布 some interesting links in comments :
  • Agner Fog 的 asm 优化指南、指令表和微架构指南:http://agner.org/optimize/
  • 英特尔优化指南:http://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-optimization-manual.pdf .
  • NUMA 窥探:http://frankdenneman.nl/2016/07/11/numa-deep-dive-part-3-cache-coherency/
  • https://software.intel.com/en-us/articles/intelr-memory-latency-checker
  • Cache Coherence Protocol and Memory Performance of the Intel Haswell-EP Architecture

  • 另请参阅 x86 中的其他内容标记维基。

    关于c++ - 为什么 std::fill(0) 比 std::fill(1) 慢?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/42558907/

    有关c++ - 为什么 std::fill(0) 比 std::fill(1) 慢?的更多相关文章

    1. ruby - 为什么我可以在 Ruby 中使用 Object#send 访问私有(private)/ protected 方法? - 2

      类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc

    2. ruby-on-rails - Rails - 子类化模型的设计模式是什么? - 2

      我有一个模型:classItem项目有一个属性“商店”基于存储的值,我希望Item对象对特定方法具有不同的行为。Rails中是否有针对此的通用设计模式?如果方法中没有大的if-else语句,这是如何干净利落地完成的? 最佳答案 通常通过Single-TableInheritance. 关于ruby-on-rails-Rails-子类化模型的设计模式是什么?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.co

    3. ruby - 什么是填充的 Base64 编码字符串以及如何在 ruby​​ 中生成它们? - 2

      我正在使用的第三方API的文档状态:"[O]urAPIonlyacceptspaddedBase64encodedstrings."什么是“填充的Base64编码字符串”以及如何在Ruby中生成它们。下面的代码是我第一次尝试创建转换为Base64的JSON格式数据。xa=Base64.encode64(a.to_json) 最佳答案 他们说的padding其实就是Base64本身的一部分。它是末尾的“=”和“==”。Base64将3个字节的数据包编码为4个编码字符。所以如果你的输入数据有长度n和n%3=1=>"=="末尾用于填充n%

    4. ruby - 解析 RDFa、微数据等的最佳方式是什么,使用统一的模式/词汇(例如 schema.org)存储和显示信息 - 2

      我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i

    5. ruby - 为什么 4.1%2 使用 Ruby 返回 0.0999999999999996?但是 4.2%2==0.2 - 2

      为什么4.1%2返回0.0999999999999996?但是4.2%2==0.2。 最佳答案 参见此处:WhatEveryProgrammerShouldKnowAboutFloating-PointArithmetic实数是无限的。计算机使用的位数有限(今天是32位、64位)。因此计算机进行的浮点运算不能代表所有的实数。0.1是这些数字之一。请注意,这不是与Ruby相关的问题,而是与所有编程语言相关的问题,因为它来自计算机表示实数的方式。 关于ruby-为什么4.1%2使用Ruby返

    6. ruby-on-rails - 如何优雅地重启 thin + nginx? - 2

      我的瘦服务器配置了nginx,我的ROR应用程序正在它们上运行。在我发布代码更新时运行thinrestart会给我的应用程序带来一些停机时间。我试图弄清楚如何优雅地重启正在运行的Thin实例,但找不到好的解决方案。有没有人能做到这一点? 最佳答案 #Restartjustthethinserverdescribedbythatconfigsudothin-C/etc/thin/mysite.ymlrestartNginx将继续运行并代理请求。如果您将Nginx设置为使用多个上游服务器,例如server{listen80;server

    7. ruby - ruby 中的 TOPLEVEL_BINDING 是什么? - 2

      它不等于主线程的binding,这个toplevel作用域是什么?此作用域与主线程中的binding有何不同?>ruby-e'putsTOPLEVEL_BINDING===binding'false 最佳答案 事实是,TOPLEVEL_BINDING始终引用Binding的预定义全局实例,而Kernel#binding创建的新实例>Binding每次封装当前执行上下文。在顶层,它们都包含相同的绑定(bind),但它们不是同一个对象,您无法使用==或===测试它们的绑定(bind)相等性。putsTOPLEVEL_BINDINGput

    8. ruby - Infinity 和 NaN 的类型是什么? - 2

      我可以得到Infinity和NaNn=9.0/0#=>Infinityn.class#=>Floatm=0/0.0#=>NaNm.class#=>Float但是当我想直接访问Infinity或NaN时:Infinity#=>uninitializedconstantInfinity(NameError)NaN#=>uninitializedconstantNaN(NameError)什么是Infinity和NaN?它们是对象、关键字还是其他东西? 最佳答案 您看到打印为Infinity和NaN的只是Float类的两个特殊实例的字符串

    9. ruby-on-rails - 如果 Object::try 被发送到一个 nil 对象,为什么它会起作用? - 2

      如果您尝试在Ruby中的nil对象上调用方法,则会出现NoMethodError异常并显示消息:"undefinedmethod‘...’fornil:NilClass"然而,有一个tryRails中的方法,如果它被发送到一个nil对象,它只返回nil:require'rubygems'require'active_support/all'nil.try(:nonexisting_method)#noNoMethodErrorexceptionanymore那么try如何在内部工作以防止该异常? 最佳答案 像Ruby中的所有其他对象

    10. ruby - 为什么 SecureRandom.uuid 创建一个唯一的字符串? - 2

      关闭。这个问题需要detailsorclarity.它目前不接受答案。想改进这个问题吗?通过editingthispost添加细节并澄清问题.关闭8年前。Improvethisquestion为什么SecureRandom.uuid创建一个唯一的字符串?SecureRandom.uuid#=>"35cb4e30-54e1-49f9-b5ce-4134799eb2c0"SecureRandom.uuid方法创建的字符串从不重复?

    随机推荐