为什么编译器在这里错过矢量化?

2024-05-24

考虑以下valarray类:

#include <stdlib.h>

struct va
{
    void add1(const va& other);
    void add2(const va& other);

    size_t* data;
    size_t  size;
};

void va::add1(const va& other) {
    for (size_t i = 0; i < size; ++i) {
        data[i] += other.data[i];
    }
}

void va::add2(const va& other){
    for (size_t i = 0, s = size; i < s; ++i) {
        data[i] += other.data[i];
    }
}

The add2函数针对不同的编译器(MSVC、Clang、GCC、ICC)进行矢量化,而add1不是。看https://godbolt.org/z/c61qvrrbv https://godbolt.org/z/c61qvrrbv

这是通过潜在的别名来解释的:编译器无法证明所指向的元素之一data不是size itself.

然而,也存在潜在的重叠元素data and other.data。对于 MSVC,这些元素和指针本身可能存在别名,因为它没有利用严格别名规则。这适用于两者add1 and add2.

编译器正在检查他们怀疑的所有可能的别名,并执行向量化操作add2.

他们为什么不添加更多检查并进行优化add1?


看起来编译器无法意识到(并且不添加代码来检查)data[i]从不指向this->size.这将使循环行程计数在第一次迭代之前无法计算,这是除 ICC 之外任何主流编译器都无法处理的问题。

希望编译器能够在应用矢量化逻辑之前学会检查可能的重叠,例如(.data > this) || (.data+.size) < this;希望有一种有效的方法来做到这一点。他们已经发明了代码来检查两个数组之间的重叠add2.

(启动时需要的检查代码越多,向量化本身的利润就越高;与 128 位向量相比,64 位标量元素在基线 x86-64 上并没有那么糟糕,尤其是当编译器不这样做时从PGO得知,尺寸通常不小,而且循环很热。我尝试过gcc -march=icelake-client and -march=znver4不仅启用 AVX2,还为具有非常好的向量吞吐量和缓存/内存带宽的 CPU 设置调整启发式。但仍然没有乐趣,所以这种可能的混叠可能是一个完整的障碍,而不是一个启发式的决定。)


Asm 证据表明编译器担心别名this->size

注意GCC的循环分支是如何的cmp rax, QWORD PTR [rdi+8], where rax holds i and [rdi+8] is this->size(x86-64 SysV 调用约定),因此每次迭代都会重新加载它。如果我们编译-O3 -fno-tree-vectorize,我们看到 GCC 的add2在循环之前将大小加载到寄存器中,与循环内的大小进行比较,即提升负载。事实上,GCC 并没有这样做add1是一个非常明显的迹象表明它认为data[i] += ...可以修改this->size.

# GCC's add1 inner loop with -O3   -march=icelake-client
.L3:
        mov     rcx, QWORD PTR [rsi+rax*8]
        add     QWORD PTR [rdx+rax*8], rcx
        inc     rax
        cmp     rax, QWORD PTR [rdi+8]
        jb      .L3

另外,将类型更改为unsigned *data;或任何不能指向size_t让 GCC、Clang 和 ICC 自动矢量化add1. Using -fno-strict-aliasing再次禁用矢量化。 (并且使编译器额外“偏执”,重新加载this->data and other.data每次迭代,就像 MSVC 一直在做的那样。也打破了矢量化add2对于那些编译器。)

更改指针类型对 MSVC 没有帮助,因为它不进行基于类型的别名分析;它总是表现得像gcc -fno-strict-aliasing。 MSVC 的add2我认为,已经检查的不仅仅是指向数组的重叠;可能其中一些额外的 cmp/jcc 正在检查this->data[i] += ...不会改变.data指针指向this or other.


std::vector没有size_t会员,通常会避免这种情况

std::vector<size_t>不会有这个问题(至少在非 MSVC 编译器中),因为基于类型的别名知道size_t *不能指向另一个指针。std::vector通常存储三个指针:.begin, .end, and end_of_capacity,所以尺寸信息是end-begin,不是直接存储大小的成员。


对于迭代一个数组,通常至少与递增指针一样有效for (... ; ptr < endp ; ptr++) *ptr所以你没有使用索引寻址模式。大概就是这个原因std::vector通常是这样布局的,而不是一个指针和两个size_t成员。

有些 RISC 机器甚至没有双寄存器索引寻址模式。对于迭代两个数组,一些 CPU 使用更少的指令会做得更好,只需增加一个索引而不是两个指针增量,但这取决于微体系结构,例如一些 x86 CPU 未层压 https://stackoverflow.com/questions/26046634/micro-fusion-and-addressing-modes add reg, [reg + reg]在后端分成 2 个 uop,而不是保持微融合,尤其是对于 3 操作数 AVX 指令。

使用索引寻址在 CPU 上循环两个数组的一种有效方法(在 asm 中)是相对于另一个数组对一个数组进行寻址。在 C++ 中执行此操作是 UB 的行为,并且会混淆您的代码,因此编译器应该为您做这些事情。sub rsi, rdi在循环之外(减去指针),那么循环体可以是mov eax, [rsi + rdi](第二个数组 = 差值 + 第一个数组)/add [rdi], eax(第一个数组)/add rdi, 8(增加指针,该指针也是另一个数组的索引。)

MSVC 实际上会进行其他编译器尚未采用的优化。 (Godbolt https://godbolt.org/#z:OYLghAFBqd5QCxAYwPYBMCmBRdBLAF1QCcAaPECAMzwBtMA7AQwFtMQByARg9KtQYEAysib0QXACx8BBAKoBnTAAUAHpwAMvAFYTStJg1DIApACYAQuYukl9ZATwDKjdAGFUtAK4sGIMwDMpK4AMngMmAByPgBGmMQSAGykAA6oCoRODB7evv5BaRmOAmER0SxxCVzJdpgOWUIETMQEOT5%2BgbaY9sUMjc0EpVGx8Um2TS1teZ0KE4PhwxWj1QCUtqhexMjsHOYB4cjeWADUJgFus%2Bi0eDEAdAhn2CYaAILPLwD0H8ceLCl08QUx1mdFox1QKUcLDwAC9MMcAO6EBAbAjHAD66OImFmxDwDlIxxiXjRhGOwEwBCBePQ4KoxwICHhDFQeCUdOOmFUBGITGOyCZyAA1uFgO8AG6s2n8VAQcJogBUmOxuPxaKYhPlxyVWJxPLVRJWJgA7FZXscLcd%2BMRjnLBMc8GcACIaU4BCwOs5uLgaMySDRnD2OyzWI2m96WyPHJgmACsFkdsadpxDAWTMTjCbjTsDEctJpzrwL73euK8DmO4pjRfD5stkrwtKY6HQXAgaAYs0rMbMiXBjPiRvdeYtDabLbM7YEXar5j7qAHxCHZredYtXy8nbwwAitIV6CYTVza%2BBsMw6MVB6P7vX32VerxFaZ2OO6FQOIYYA4aKZtBSI9POELwtDI4WPN5jRzYci1eMduxAEBm1bKdOzRWde37Z8VlOWsXktL5APPNFQPhZ0GQQNkAFpHhI8DI2tW0SOAvA3WTAMbxYr1CMDFMrEsPBsJNFco0tK8Y3jRNk2sMiF2fW4xMzSS6PzSCS1UmCXjgqsEKQycOxnHtEnvVUK1kwchIAhiICY0lWOODRCSBMjaI4t03GBHjpP4wTcJEi0FIk7NeJkxd5MPcSsyTZSLWLGtCxeDg1loThY14PwOC0UhUE4NwvI9BQNi2UjAh4UgCE0RK1iFfxEluAAORJjWNABODREg0Y1EgCOrmuNIJko4SQ0oqrLOF4BQQAc8qMsS0g4FgGBEBQVA/gBMgKCnVb6AScVkBSFJ0XFLhmvRAwCD1dFVESaQsHFfFMAANTwTAEQAeRSRhOFKmhaHO4gJogGIRpicJmgATy%2B3gQeYYgwdemJtDqabSrQFg2EEV6GFoCGZtILBiWANwxFoCbuF4LAWEMYBxFx/BsXqcUcRGrk6hJHZSvlboRuuGJeVhjwsBG/UWEhtYqAMYAFCel73s%2BsmZEEEQxHYKQFfkJQ1BG3QzH0KmUDy/QbgmyA1ghXpSY%2BV6zGOD5mgFEAXgegANMxeFQRniBpJmTa6HoshcBh3E8do9FCBZykqPRCkyAQpj8LhUnSGOGCGCPll9pGGjmOO9FqeoBH6FpU5GKpxgGHOE9mAZi6WKo1gKzZtgkJKUuG3Hso4Y4rskY4WAUXbK2O24zr1W1cEIEgUwCLgVl4aatBWNYmWbUYICq/w6tuMwuACAJmskafYy4Mweua/ROCG0gRdjBz0syjvxsmsqKrWealtR/5tvISgP7WkB8WQGYX0XBjQOVuvdaWb0PrpW%2BnQP6AMga42huDSGpBkGw3hojBwqDUbowIJjbGI18ZeEJsTUmpUKZUxpplOmmdGak0yizZAbNUGcwGplHmfMwYC3ZnPPEIsyZiwllLZ6UC5bfVkErcQqt%2BCCEUCodQuNdD9T1qYEMlhDYxGNmvLKkIsik0oq9AIxxKKU3FKoV2WUPZe20fXbomdnAQFcBXYIgca6RwTtHXoLivFZHcenPOvRC6tGDnkSu9j859DmP40uVdJihPjmXIu4cS7N3WI3FWLcOCpVIHfN2nAu4NUotdB0yBkDHCAdvW4xpbiuggOPIgNo9gzzni/JemAV4JB0QNS%2B19b4jQfrYJ%2B89KqkGqtPW4%2B8AigLqpIWMvUNB1Q0G1c%2BHAAht3vmNZ%2BM1X6LTfhAJAv8v4bSOaMSWzAUgKBRAQPGmA7rbEgbLGBvAfrwMoIgzK6CcalS%2BZgpGOCVp4IITjGhmACZE1oCTVBlCjDUPJngemjh6HM1UKzc6rDBBc1xpw8GPDUHC1FnwYRjzoGoNkcIUQ0jpDkvkZrJRegDBGH1uomwPNbG6PNpwQxxiLSmLtg8NMMIGAe0vu7eINj4B2L9o45xCTQ5uJSbXKOSdvFys8SqvxiqPEZ0icElxgSs7Vy1QE7OaqknzDKKkme6SippIGjkvJo1O6qCKSU4AZTbQ8k3EKbC9T8CNKni07ZC914TOuqA5q3U6qxljL2a60gem8D6bkgZWyJpTRfnNPZhyVqf3iN/TaeaEiHCpheYg3rbn3MeqIp5ZK4GAnecDUGsNUG/IRv8%2BWuDGD4KxiC8mYKSEQqhfLGF1Mdg0IRXQpmuMmEsPlmw7mNwuF4vlgSwRRKmCSxJeIl5kjKUq2pbIWlijMq6ATaog2bLJUcv0Vyoxlo%2BVbAFU6F1iRimSEouKYxbtrGNm9jog1MrA4uLDpapV6qihZB8RqkoxrYkRKCaa3IiTANRKNWB7VcSQnIdztEuDaSG62utfajZ%2BTnWup7iWownry0MB9WPf1k9mmz2DaM5eWAulZN6SAG%2BKb25puGW0sZIBJBcFuFwOq0blmRt7LGAIbUE2cHWXxzZHBWk7KyZYx1gyRmL1IB7DIzhJBAA%3D)

// Compilers still optimize without __restrict, but it gets rid of the noise of extra checking
void foo(int *__restrict a, int *__restrict b){
    for (int i=0 ; i<10240; i++){
        a[i] += b[i];
    }
}
void foo(int * __restrict,int * __restrict) PROC                              ; foo, COMDAT
        lea     rax, QWORD PTR [rcx+32]
        sub     rdx, rcx       ;;;; <---- Pointer subtraction
        mov     ecx, 320                      ; 00000140H
        npad    4
$LL4@foo:
        vmovdqu ymm1, YMMWORD PTR [rax-32]            ;; unrolled with 4 vectors
        vpaddd  ymm1, ymm1, YMMWORD PTR [rdx+rax-32]
        vmovdqu YMMWORD PTR [rax-32], ymm1

        vmovdqu ymm2, YMMWORD PTR [rax]
        vpaddd  ymm1, ymm2, YMMWORD PTR [rdx+rax]
        vmovdqu YMMWORD PTR [rax], ymm1

        vmovdqu ymm1, YMMWORD PTR [rax+32]
        vpaddd  ymm1, ymm1, YMMWORD PTR [rdx+rax+32]
        vmovdqu YMMWORD PTR [rax+32], ymm1

        vmovdqu ymm1, YMMWORD PTR [rax+64]
        vpaddd  ymm1, ymm1, YMMWORD PTR [rdx+rax+64]
        vmovdqu YMMWORD PTR [rax+64], ymm1

        lea     rax, QWORD PTR [rax+128]
        sub     rcx, 1
        jne     SHORT $LL4@foo
        vzeroupper
        ret     0

不幸的是,MSVC 搞反了,并使用双寄存器寻址模式作为内存源操作数vpaddq. It'll 未层压 https://stackoverflow.com/questions/26046634/micro-fusion-and-addressing-modes正在讨论/重命名为 Intel Sandybridge 系列上的 ROB,至少包括 Skylake,可能会稍后一些。但vpaddd ymm1, ymm1, [rdx]将为 1 uop。纯负载vmovdqu即使使用索引寻址模式,也始终为 1 uop。

索引存储也不理想(存储地址 uop 无法在 Haswell / Skylake 上的端口 7 上运行),MSVC 确实避免了这种情况。但它可以通过执行纯负载来获得两全其美的效果b[]使用索引寻址模式,然后是内存源vpadd+ 使用简单寻址模式存储,例如[rdx+32].

因此,MSVC 确实节省了一些代码大小,并且通过仅需要一个循环开销增量,在后端吞吐量方面获得了一些好处,并且在 AGU 端口争用中,因此可以在每个时钟接近 1 个向量的情况下运行,并且 L1d 缓存命中可以让它运行每个周期执行 2 次加载 + 1 次存储(Intel 的优化手册表明,由于某些未知原因,Skylake 无法完全支持 32 字节加载/存储),

使用 GCC 和 clang 等存储的索引寻址模式,它只能在 Haswell / Skylake 上以每 1.5 个周期 1 个向量运行。 (Ice Lake 有两个负载 AGU 和两个独立的存储 AGU,避免了这个瓶颈,但我不知道索引寻址模式的非分层在 Ice Lake 或 Alder Lake 上是否仍然存在。)

我不知道 MSVC 更喜欢什么lea用于递增指针。或者为了更喜欢sub ecx/jne而不是与结束指针进行比较lea在循环之前而不是mov。那么循环的结束可能是cmp rax, r8/jne或者其他东西,它会在 AMD 上融合成单个微指令,而不仅仅是英特尔。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

为什么编译器在这里错过矢量化? 的相关文章

  • Python Selenium:如何在文本文件中打印网站上的值?

    我正在尝试编写一个脚本 该脚本将从 tulsaspca org 网站获取以下 6 个值并将其打印在 txt 文件中 最终输出应该是 905 4896 7105 23194 1004 42000 放置的动物 的 HTML span class
  • 如何防止用户控件表单在 C# 中处理键盘输入(箭头键)

    我的用户控件包含其他可以选择的控件 我想实现使用箭头键导航子控件的方法 问题是家长控制拦截箭头键并使用它来滚动其视图什么是我想避免的事情 我想自己解决控制内容的导航问题 我如何控制由箭头键引起的标准行为 提前致谢 MTH 这通常是通过重写
  • 如何在发布期间复制未版本化的测试资源:执行?

    我的问题与 Maven 在发布时不会复制未跟踪的资源 https stackoverflow com questions 10378708 maven doesnt copy untracked resources while releas
  • 如何确定所有角度2分量都已渲染?

    当所有 Angular2 组件完成渲染时 是否会触发一个角度事件 For jQuery 我们可以用 function 然而 对于 Angular2 当domready事件被触发 html 只包含角度组件标签 每个组件完成渲染后 domrea
  • TIFF 元数据的最大大小是多少?

    TIFF 文件元数据的单个字段中可以合并的元数据数量是否有最大限制 我想在 ImageDescription 字段中存储大文本 最多几 MB 没有具体的最大限制ImageDescription但是 整个 TIFF 文件存在最大文件大小 该最
  • 如何在执行新操作时取消先前操作的执行?

    我有一个动作创建器 它会进行昂贵的计算 并在每次用户输入内容时调度一个动作 基本上是实时更新 但是 如果用户输入多个内容 我不希望之前昂贵的计算完全运行 理想情况下 我希望能够取消执行先前的计算并只执行当前的计算 没有内置功能可以取消Pro
  • 使用.NET技术录制屏幕视频[关闭]

    Closed 这个问题正在寻求书籍 工具 软件库等的推荐 不满足堆栈溢出指南 help closed questions 目前不接受答案 有没有一种方法可以使用 NET 技术来录制屏幕 无论是桌面还是窗口 我的目标是免费的 我喜欢小型 低
  • 如何从日期中查找该月的最后一天?

    如何在 PHP 中获取该月的最后一天 Given a date 2009 11 23 我要2009 11 30 并给出 a date 2009 12 23 我要2009年12月31日 t返回给定日期所在月份的天数 请参阅的文档date ht
  • PHPUnit 和 Zend Framework assertRedirectTo() 问题

    我在创建的测试中遇到了 assertRedirectTo 问题 下面是我使用的代码 public function testLoggedInIndexAction this gt dispatch this gt assertControl
  • 如何使用asm.js进行测试和开发?

    最近我读到asm js规范 看起来很酷 但是是否有任何环境 工具来开发和测试这个工具 这还只是处于规范阶段吗 您可以尝试使用 emscripten 和 ASM JS 1 并从侧分支在 firefox 构建中运行它 有关 asm js 的链接
  • 从超立方体图像中获取文本的确切位置

    使用 tesseract 中的 GetHOCRText 0 方法 我能够检索 html 中的文本 并在 webview 中呈现 html 时 我能够获取文本 但图像中文本的位置与输出不同 任何想法都非常有帮助 tesseract gt Se
  • CSS溢出文本显示在几行中,没有断字

    我有一些长文本显示在 div 中 该 div 具有固定的宽度和高度 我希望文本显示在几行上 作为 div 高度 并且句子单词不会中断 一行中的单词前缀和下一行中的继续 此外 我想在末尾添加省略号最后一句话 CSS white space n
  • 节拍匹配算法

    我最近开始尝试创建一个移动应用程序 iOS Android 它将自动击败比赛 http en wikipedia org wiki Beatmatching http en wikipedia org wiki Beatmatching 两
  • 用于验证目的的动态查找方法

    我正在使用 Ruby on Rails 3 0 7 我想在运行时查找一些记录以进行验证 但为该查找方法传递 设置一个值 也就是说 在我的班级中 我有以下内容 class Group lt lt ActiveRecord Base valid
  • neo4j - python 驱动程序,服务不可用

    我对 neo4j 非常陌生 我正在尝试建立从 python3 6 到 neo4j 的连接 我已经安装了驱动程序 并且刚刚开始执行第一步 导入请求 导入操作系统 导入时间 导入urllib 从 neo4j v1 导入 GraphDatabas
  • 使用 xpath 和 vtd-xml 以字符串形式获取元素的子节点和文本

    这是我的 XML 的一部分
  • 如何使用 Pycharm 安装 tkinter? [复制]

    这个问题在这里已经有答案了 I used sudo apt get install python3 6 tk而且效果很好 如果我在终端中打开 python Tkinter 就可以工作 但我无法将其安装在我的 Pycharm 项目上 pip
  • 升级到 Rails 6 时是否有一种编程方法可以检测 Zeitwerk::NameError?

    我目前正在将旧的 Rails 应用程序迁移到 Rails 6 好像项目中有些文件和里面定义的类不一致 运行应用程序测试时我没有看到此错误 但部署后我收到如下错误 Zeitwerk NameError expected file app my
  • 在 Nexus 7 2013 上更改方向时 CSS 媒体查询不起作用

    我目前正在我的笔记本电脑 台式电脑和 Nexus 7 2013 上测试 CSS 媒体查询 除了 Nexus 7 之外 它们在台式机和笔记本电脑上都运行良好 当我更改方向时 除非刷新页面 否则样式不会应用 例如 以纵向模式握住设备时 页面正常
  • 强制 Listview 不重复使用视图(复选框)

    我做了一个定制Listview 没有覆盖getView 方法 Listview 中的每个项目都具有以下布局 联系布局 xml

随机推荐