为什么 GCC 减去 1 并比较 <= 2?在汇编中使用 2 的幂时 cmp 是否更快?

2024-03-28

我正在编写一些代码来将屏幕清除为特定颜色。 C++代码:

void clear_screen(unsigned int color, void *memory, int height, int width) {
  unsigned int *pixel = (unsigned int *)memory;
  for (auto y = 0; y < height; y++)
    for (auto x = 0; x < width; x++)
      *pixel++ = color;
}

我使用g++和objconv生成相应的程序集。这就是我得到的,我也评论了我认为其中一些行的作用。

renderer_clear_screen:
        push    r13                                     
        push    r12                                     
        push    rbp                                     
        push    rdi                                     
        push    rsi                                     
        push    rbx                                     
        mov     r11d, ecx            ; move the color into r11d
        mov     ebx, r8d             ; move the height into ebx
        mov     rcx, rdx             ; 000E _ 48: 89. D1st
        test    r8d, r8d             ; 
        jle     _cls_return          ; basically, return if width or height is 0
        test    r9d, r9d             ; ( window minimized )
        jle     _cls_return          ;
        mov     r8d, r9d             ; height = width
        mov     esi, r9d             ; esi = width
        mov     edi, r9d             ; edi = width
        xor     r10d, r10d           ; r10d = 0
        shr     esi, 2               ; esi = width / 2
        movd    xmm1, r11d           ; move the lower 32-bits of the color into xmm1
        lea     r12d, [r9-1]         ; r12d = width - 1
        shl     rsi, 4               ; 003F _ 48: C1. E6, 04
        mov     ebp, r8d             ; 0043 _ 44: 89. C5
        shl     rdi, 2               ; 0046 _ 48: C1. E7, 02
        pshufd  xmm0, xmm1, 0        ; 004A _ 66: 0F 70. C1, 00
        shl     rbp, 2               ; 004F _ 48: C1. E5, 02

ALIGN   8
?_001:  cmp     r12d, 2                                
        jbe     ?_006                ; if (width - 1 <= 2) { ?_006 }
        mov     rax, rcx             ; 005E _ 48: 89. C8
        lea     rdx, [rcx+rsi]       ; 0061 _ 48: 8D. 14 31

ALIGN   8
?_002:  movups  oword [rax], xmm0    ; 0068 _ 0F 11. 00
        add     rax, 16              ; 006B _ 48: 83. C0, 10
        cmp     rdx, rax             ; 006F _ 48: 39. C2
        jnz     ?_002                ; 0072 _ 75, F4
        lea     rdx, [rcx+rbp]       ; 0074 _ 48: 8D. 14 29
        mov     eax, r8d             ; 0078 _ 44: 89. C0
        cmp     r9d, r8d             ; 007B _ 45: 39. C1
        jz      ?_004                ; 007E _ 74, 1C
?_003:  lea     r13d, [rax+1H]       ; 0080 _ 44: 8D. 68, 01
        mov     dword [rdx], r11d    ; 0084 _ 44: 89. 1A
        cmp     r13d, r9d            ; 0087 _ 45: 39. CD
        jge     ?_004                ; 008A _ 7D, 10
        add     eax, 2               ; 008C _ 83. C0, 02
        mov     dword [rdx+4H], r11d ; 008F _ 44: 89. 5A, 04
        cmp     r9d, eax             ; 0093 _ 41: 39. C1
        jle     ?_004                ; 0096 _ 7E, 04
        mov     dword [rdx+8H], r11d ; 0098 _ 44: 89. 5A, 08
?_004:  add     r10d, 1              ; 009C _ 41: 83. C2, 01
        add     rcx, rdi             ; 00A0 _ 48: 01. F9
        cmp     ebx, r10d            ; 00A3 _ 44: 39. D3
        jnz     ?_001                ; 00A6 _ 75, B0
_cls_return: 
        pop     rbx                  ;
        pop     rsi                  ;
        pop     rdi                  ;
        pop     rbp                  ;
        pop     r12                  ;
        pop     r13                  ; pop all the saved registers
        ret                          ; 

?_006:  ; Local function
        mov     rdx, rcx             ; 00B1 _ 48: 89. CA
        xor     eax, eax             ; 00B4 _ 31. C0
        jmp     ?_003                ; 00B6 _ EB, C8

Now, in ?_001,编译器比较width - 1 to 2,这与比较相同的事情width to 3。我的问题是,与-O3,为什么编译器选择了两个而不是三个,浪费了一个lea(移动width - 1 into r12d).
对我来说唯一有意义的是,比较两个的幂在某种程度上更快。或者也许这是编译器的怪癖?


GCC 调整比较常量的通常原因是创建更小的立即数,这有助于它适合任何宽度的立即数。理解 if (a>=3) 的 gcc 输出 https://stackoverflow.com/q/46412381 / 在比较中,GCC 似乎更喜欢小的立即值。有办法避免这种情况吗? https://stackoverflow.com/q/59117603(它总是这样做,而不是检查这个常量在目标 ISA 上是否真的有用。)这种启发式方法适用于大多数 ISA,但有时不适用于 AArch64 或 ARM Thumb,它们可以将一些立即数编码为位范围/位-pattern,因此并不总是数值越小越好。


The width-1 is not其中一部分。这-1是一个的一部分范围检查 https://stackoverflow.com/questions/5196527/double-condition-checking-in-assembly跳过自动矢量化循环(一次 16 个字节)movups)并直接进行清理,1..3 标量存储。

似乎正在检查width >= 1 && width <= 3,即需要清理,但总大小小于完整的向量宽度。它不等于已签名或未签名width <= 3 for width=0。注意无符号比较:0 - 1上面是2U, 因为-1U是 UINT_MAX。

但已经排除了width <= 0 with test r9d, r9d / jle _cls_return,所以 GCC 最好只检查width <= 3U而不是做额外的工作来从范围检查中排除零。 (一个lea,并保存/恢复未使用的 R12!)

(清理也可能看起来过于复杂,例如使用movq [rdx], xmm0如果需要超过 1 个 uint,并且针对各种情况进行一些奇怪的分支。更好的是,如果总大小 >= 4 uints,只需再做一次movups结束于范围末尾,可能与之前的商店重叠。)

是的,这是一个错过的优化,您可以报告它https://gcc.gnu.org/bugzilla/enter_bug.cgi?product=gcc https://gcc.gnu.org/bugzilla/enter_bug.cgi?product=gcc(现在您知道这是一个错过的优化;最好先在这里询问,而不是在没有首先弄清楚是否可以避免该指令的情况下提交错误。)


对我来说唯一有意义的是,比较两个的幂在某种程度上更快。

不,不是更快,而是更快。cmp性能根本不依赖于数据。 (没有整数指令,除非有时[i]div。而在 Zen3 之前的 AMD CPU 上,pext / pdep。但无论如何,这不是简单的整数加/比较/移位的东西。看https://uops.info/ https://uops.info/).


顺便说一句,我们可以重现您的Godbolt 上的 GCC asm 输出 https://godbolt.org/#g:!((g:!((g:!((h:codeEditor,i:(filename:%271%27,fontScale:14,fontUsePx:%270%27,j:1,lang:c%2B%2B,selection:(endColumn:22,endLineNumber:2,positionColumn:22,positionLineNumber:2,selectionStartColumn:22,selectionStartLineNumber:2,startColumn:22,startLineNumber:2),source:%27%0A__attribute__((ms_abi))%0Avoid+clear_screen(unsigned+int+color,+void+*memory,+int+height,+int+width)+%7B%0A++unsigned+int+*pixel+%3D+(unsigned+int+*)memory%3B%0A++for+(auto+y+%3D+0%3B+y+%3C+height%3B+y%2B%2B)%0A++++for+(auto+x+%3D+0%3B+x+%3C+width%3B+x%2B%2B)%0A++++++*pixel%2B%2B+%3D+color%3B%0A%7D%0A%27),l:%275%27,n:%270%27,o:%27C%2B%2B+source+%231%27,t:%270%27)),k:35.58012044257613,l:%274%27,n:%270%27,o:%27%27,s:0,t:%270%27),(g:!((h:compiler,i:(compiler:g121,filters:(b:%270%27,binary:%271%27,commentOnly:%270%27,demangle:%271%27,directives:%270%27,execute:%271%27,intel:%270%27,libraryCode:%270%27,trim:%271%27),flagsViewOpen:%271%27,fontScale:14,fontUsePx:%270%27,j:1,lang:c%2B%2B,libs:!(),options:%27-O3%27,selection:(endColumn:1,endLineNumber:1,positionColumn:1,positionLineNumber:1,selectionStartColumn:1,selectionStartLineNumber:1,startColumn:1,startLineNumber:1),source:1,tree:%271%27),l:%275%27,n:%270%27,o:%27x86-64+gcc+12.1+(C%2B%2B,+Editor+%231,+Compiler+%231)%27,t:%270%27)),k:34.351129485231034,l:%274%27,m:100,n:%270%27,o:%27%27,s:0,t:%270%27),(g:!((h:compiler,i:(compiler:clang1400,filters:(b:%270%27,binary:%271%27,commentOnly:%270%27,demangle:%271%27,directives:%270%27,execute:%271%27,intel:%270%27,libraryCode:%270%27,trim:%271%27),flagsViewOpen:%271%27,fontScale:14,fontUsePx:%270%27,j:2,lang:c%2B%2B,libs:!(),options:%27-O3%27,selection:(endColumn:1,endLineNumber:1,positionColumn:1,positionLineNumber:1,selectionStartColumn:1,selectionStartLineNumber:1,startColumn:1,startLineNumber:1),source:1,tree:%271%27),l:%275%27,n:%270%27,o:%27x86-64+clang+14.0.0+(C%2B%2B,+Editor+%231,+Compiler+%232)%27,t:%270%27)),header:(),k:30.06875007219285,l:%274%27,n:%270%27,o:%27%27,s:0,t:%270%27)),l:%272%27,n:%270%27,o:%27%27,t:%270%27)),version:4告诉它这个函数是__attribute__((ms_abi)),或者有一个命令行选项来设置调用约定默认值。 (它实际上只对查看 asm 有用;它仍然使用 GNU/Linux 标头和 x86-64 System V 类型宽度,例如 64 位long。只有合适的 MinGW(交叉)编译器才能向您展示 GCC 在针对 Windows 时真正会做什么。)

这是气体.intel_syntax noprefix,类似于 MASM,而不是 NASM,但只有在涉及全局变量的寻址模式中,差异才会明显。

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

为什么 GCC 减去 1 并比较 <= 2?在汇编中使用 2 的幂时 cmp 是否更快? 的相关文章

  • 如何在代码中设置控件模板?

    我在 XAML 中有这个
  • QT 5.6 QWebEngine不保存cookie

    我正在创建名为 webengine 的简单 QT 应用程序 pWebView new QWebEngineView this pWebView gt load QUrl http technoz ru pWebView gt show On
  • 从 C# 运行 32 位或 64 位 PowerShell

    我构建了一个执行 PowerShell 脚本的 32 位 NET DLL 我需要它能够以 64 位模式运行脚本and 32 bit 我已经知道如何使用命令行执行此操作 C Windows Sysnative cmd c powershell
  • 这个具有多个值(变量)的 return 语句如何工作? [复制]

    这个问题在这里已经有答案了 我试图了解 C 函数中按值传递和返回是如何发生的 我发现一段代码如下 include
  • 指向基类的基本多态指针

    虽然我已经在 C 领域工作了一段时间 但直到现在我才需要使用多态特性 而且我对它们非常感兴趣 如果我有一个基类ClassA和另一个ClassB从中衍生出来 我明白我可以拥有virtual中的成员函数ClassA即 当实施于ClassB 将被
  • 如何序列化其类相互引用的类层次结构,但避免 XmlInclude?

    我有一个类的层次结构 我想使用XmlSerializer类及其相关属性 有一个基本抽象类 然后是相当多的派生类 在下面的代码中 我已将派生类的数量减少到五个 但实际代码中还有更多 这些类形成一个层次结构 并且经常包含对层次结构中类的实例的引
  • 抑制第 3 方库控制台输出?

    我需要调用一个第三方库 该库恰好会向控制台输出一堆内容 代码就像这样 int MyMethod int a int b ThirdPartyLibrary Transform a spews unwanted console output
  • 在 C 中从数组创建子数组的最佳方法

    我有一个数组说a 3 1 2 5 我必须创建另一个数组a2 2 2 5 我尝试过的是创建一个新数组a2 只需复制所需位置范围内的所有元素即可 在C语言中还有其他方法可以实现这一点吗 memcpy a2 a 1 2 sizeof a
  • std::function 和 std::bind 行为

    我有这个代码 include
  • 获取DataGridView中特定列的值

    我的 Winforms 应用程序中有一个 datagridview 用户可以单击 datagridview 上的任意位置 然后单击按钮对该数据行执行操作 但是 为了做到这一点 我需要从该行恢复 ID 现在请记住 用户可能尚未单击 ID 列
  • C#:模拟内存泄漏

    我想用c 编写以下代码 a 模拟内存泄漏的小型控制台应用程序 b 小型控制台应用程序 它将调用上述应用程序并立即释放它 模拟管理内存泄漏问题 换句话说 应用程序 b 将不断调用并释放应用程序 a 以模拟如何遏制 叛逆 内存泄漏应用程序 而不
  • 使用 istream_iterator 范围构造时无法访问向量

    我尝试编译此代码片段 但出现编译器错误 使用 Visual Studio 2010 进行编译 include
  • 哪个对缓存最友好?

    我试图很好地掌握面向数据的设计以及如何在考虑缓存的情况下进行最佳编程 基本上有两种情况我无法完全确定哪个更好以及为什么 是拥有一个对象向量更好 还是拥有对象原子数据的多个向量更好 A 对象向量示例 struct A GLsizei mInd
  • C 结构体中的 Typedef

    首先是令我困惑的代码 typedef struct Object typedef int MyInt void destructor Object void constructor struct Object Object 为什么编译器阻止
  • 编译器如何解析在变长数组之后声明的变量的地址?

    假设我有以下函数 它使用可变长度数组 void func int size int var1 int arr size int var2 编译器如何确定地址var2 我能想到的唯一方法就是放置arr after var1 and var2
  • 重载解析:这如何不含糊不清?

    假设我们有这段代码 是从一个单独的问题复制的 namespace x void f class C void f using x f f lt 名字f在指定的行上明确指的是x f 至少根据 gcc 和 clang 为什么是x f优先于x C
  • 为什么IL代码中stloc.0后面有一个ldloc.0?

    我正在尝试通过编写小代码片段和检查编译的程序集来学习 CIL 所以我写了这个简单的 if 语句 public static void Main string args Int32 i Int32 Parse Console ReadLine
  • Eigen 如何沿特定维度连接矩阵?

    我有两个特征矩阵 我想将它们连接起来 就像在 matlab 中一样cat 0 A B eigen 有等价物吗 Thanks 您可以使用逗号初始值设定项语法 水平方向 MatrixXd C A rows A cols B cols C lt
  • 使用 winforms 、 mdi 、父子窗体,在父窗体下指定空间打开子窗体

    我有一个 winform MAINFORM 需要以此形式打开子窗体 如图所示 黑色部分是一个面板并且包含一个编号 具有多个节点的 LinkLabels 和 Treeview 在其余部分中 我想在单击面板上的链接标签时显示子表单 子表单应完全
  • REQ/REP 模式中的 ZeroMQ FiniteStateMachineException

    我有两个简单的组件 它们应该使用 REQ REP ZeroMQ 模式相互通信 服务器 REP Socket 是使用 pyzmq 在 Python 中实现的 import zmq def launch server print Launchi

随机推荐