草庐IT

c++ - 从编译器的角度来看,如何处理数组的引用,以及为什么不允许按值传递(而不是衰减)?

coder 2024-02-13 原文

正如我们所知,在 C++ 中,我们可以像 f(int (&[N]) 一样将数组的引用作为参数传递。是的,它是由 iso 标准保证的语法,但我很好奇编译器在这里是如何工作的。我找到了这个 thread ,但不幸的是,这并没有回答我的问题——编译器是如何实现这个语法的?

然后我写了一个demo,希望能从汇编语言中看到一些东西:

void foo_p(int*arr) {}
void foo_r(int(&arr)[3]) {}
template<int length>
void foo_t(int(&arr)[length]) {}
int main(int argc, char** argv)
{
    int arr[] = {1, 2, 3};
    foo_p(arr);
    foo_r(arr);
    foo_t(arr);
   return 0;
}

最初,我 猜测 它仍然会衰减到指针,但会通过寄存器隐式传递长度,然后在函数体中变回数组。但是汇编代码告诉我这不是真的
void foo_t<3>(int (&) [3]):
  push rbp #4.31
  mov rbp, rsp #4.31
  sub rsp, 16 #4.31
  mov QWORD PTR [-16+rbp], rdi #4.31
  leave #4.32
  ret #4.32

foo_p(int*):
  push rbp #1.21
  mov rbp, rsp #1.21
  sub rsp, 16 #1.21
  mov QWORD PTR [-16+rbp], rdi #1.21
  leave #1.22
  ret #1.22

foo_r(int (&) [3]):
  push rbp #2.26
  mov rbp, rsp #2.26
  sub rsp, 16 #2.26
  mov QWORD PTR [-16+rbp], rdi #2.26
  leave #2.27
  ret #2.27

main:
  push rbp #6.1
  mov rbp, rsp #6.1
  sub rsp, 32 #6.1
  mov DWORD PTR [-16+rbp], edi #6.1
  mov QWORD PTR [-8+rbp], rsi #6.1
  lea rax, QWORD PTR [-32+rbp] #7.15
  mov DWORD PTR [rax], 1 #7.15
  lea rax, QWORD PTR [-32+rbp] #7.15
  add rax, 4 #7.15
  mov DWORD PTR [rax], 2 #7.15
  lea rax, QWORD PTR [-32+rbp] #7.15
  add rax, 8 #7.15
  mov DWORD PTR [rax], 3 #7.15
  lea rax, QWORD PTR [-32+rbp] #8.5
  mov rdi, rax #8.5
  call foo_p(int*) #8.5
  lea rax, QWORD PTR [-32+rbp] #9.5
  mov rdi, rax #9.5
  call foo_r(int (&) [3]) #9.5
  lea rax, QWORD PTR [-32+rbp] #10.5
  mov rdi, rax #10.5
  call void foo_t<3>(int (&) [3]) #10.5
  mov eax, 0 #11.11
  leave #11.11
  ret #11.11

live demo

我承认我不熟悉汇编语言,但很明显,三个函数的汇编代码是一样的!因此,在汇编代码之前必须发生一些事情。无论如何,与数组不同,指针对长度一无所知,对吗?

问题:
  • 编译器在这里是如何工作的?
  • 现在标准允许通过引用传递数组,这是否意味着实现起来很简单?如果是这样,为什么不允许按值传递?


  • 对于 Q2,我的猜测是之前的 C++ 和 C 代码的复杂性。毕竟,int[] 在函数参数中等于 int* 一直是传统。也许一百年后,它会被弃用?

    最佳答案

    在汇编语言中,对数组的 C++ 引用与指向第一个元素的指针相同。

    即使是 C99 int foo(int arr[static 3]) 仍然只是 asm 中的一个指针。 static syntax 向编译器保证即使 C 抽象机不访问某些元素,它也可以安全地读取所有 3 个元素,因此例如它可以对 cmov 使用无分支 if

    调用者不会在寄存器中传递长度,因为它是编译时常量,因此在运行时不需要。

    您可以按值传递数组,但前提是它们位于结构体或 union 体中。在这种情况下,不同的调用约定有不同的规则。 What kind of C11 data type is an array according to the AMD64 ABI

    您几乎从不想按值传递数组,因此 C 没有语法是有道理的,而 C++ 也从未发明任何语法。通过常量引用(即 const int *arr )传递效率更高;只是一个指针 arg。

    通过启用优化来消除编译器噪音:

    我将您的代码放在 Godbolt 编译器资源管理器中,使用 gcc -O3 -fno-inline-functions -fno-inline-functions-called-once -fno-inline-small-functions 进行编译以阻止它内联函数调用。这消除了 -O0 调试构建和帧指针样板的所有噪音。 (我只是在手册页中搜索了 inline 并禁用了内联选项,直到我得到了我想要的。)

    您可以在函数定义上使用 GNU C -fno-inline-small-functions 来禁用特定函数的内联,而不是 __attribute__((noinline)) 等,即使它们是 static

    我还添加了对没有定义的函数的调用,因此编译器需要在内存中具有具有正确值的 arr[],并在其中两个函数中为 arr[4] 添加了一个存储。这让我们测试编译器是否警告超出数组边界。

    __attribute__((noinline, noclone)) 
    void foo_p(int*arr) {(void)arr;}
    void foo_r(int(&arr)[3]) {arr[4] = 41;}
    
    template<int length>
    void foo_t(int(&arr)[length]) {arr[4] = 42;}
    
    void usearg(int*); // stop main from optimizing away arr[] if foo_... inline
    
    int main()
    {
        int arr[] = {1, 2, 3};
        foo_p(arr);
        foo_r(arr);
        foo_t(arr);
        usearg(arr);
       return 0;
    }
    

    gcc7.3 -O3 -Wall -Wextra without function inlining, on Godbolt :由于我从您的代码中消除了未使用的参数警告,我们得到的唯一警告来自模板,而不是来自 foo_r :
    <source>: In function 'int main()':
    <source>:14:10: warning: array subscript is above array bounds [-Warray-bounds]
         foo_t(arr);
         ~~~~~^~~~~
    

    汇编输出是:
    void foo_t<3>(int (&) [3]) [clone .isra.0]:
        mov     DWORD PTR [rdi], 42       # *ISRA.3_4(D),
        ret
    foo_p(int*):
        rep ret
    foo_r(int (&) [3]):
        mov     DWORD PTR [rdi+16], 41    # *arr_2(D),
        ret
    
    main:
        sub     rsp, 24             # reserve space for the array and align the stack for calls
        movabs  rax, 8589934593     # this is 0x200000001: the first 2 elems
        lea     rdi, [rsp+4]
        mov     QWORD PTR [rsp+4], rax    # MEM[(int *)&arr],  first 2 elements
        mov     DWORD PTR [rsp+12], 3     # MEM[(int *)&arr + 8B],  3rd element as an imm32
        call    foo_r(int (&) [3])
        lea     rdi, [rsp+20]
        call    void foo_t<3>(int (&) [3]) [clone .isra.0]    #
        lea     rdi, [rsp+4]      # tmp97,
        call    usearg(int*)     #
        xor     eax, eax  #
        add     rsp, 24   #,
        ret
    

    foo_p() 的调用仍然被优化掉了,可能是因为它没有做任何事情。 (我没有禁用过程间优化,甚至 noinlinenoclone 属性也没有阻止。)将 *arr=0; 添加到函数体会导致从 main 调用它(在 rdi 中传递一个指针,就像其他 2 )。

    请注意解散函数名称上的 clone .isra.0 注释:gcc 对函数进行了定义,该函数采用指向 arr[4] 而不是基本元素的指针。这就是为什么有一个 lea rdi, [rsp+20] 来设置 arg,以及为什么商店使用 [rdi] 来取消引用而不发生位移的点。 __attribute__((noclone)) 会阻止它。

    这种过程间优化非常简单,在这种情况下节省了 1 字节的代码大小(只是克隆中寻址模式中的 disp8),但在其他情况下可能很有用。调用者需要知道它是函数修改版本的定义,比如 void foo_clone(int *p) { *p = 42; } ,这就是为什么它需要在损坏的符号名称中对其进行编码。

    如果您在一个文件中实例化模板并从另一个看不到定义的文件中调用它,那么如果没有链接时优化,gcc 将只需要调用常规名称并像函数一样传递一个指向数组的指针书面。

    IDK 为什么 gcc 为模板而不是引用执行此操作。这可能与它警告模板版本而不是引用版本有关。或者它可能与 main 推导模板有关?

    顺便说一句,实际上可以让它运行得稍微快一点的 IPO 是让 main 使用 mov rdi, rsp 而不是 lea rdi, [rsp+4] 。即,将 &arr[-1] 作为函数 arg,因此克隆将使用 mov dword ptr [rdi+20], 42

    但这仅对 main 之类的调用者有用,它们在 rsp 之上分配了一个数组 4 个字节,我认为 gcc 只是在寻找使函数本身更高效的 IPO,而不是某个特定调用者中的调用序列。

    关于c++ - 从编译器的角度来看,如何处理数组的引用,以及为什么不允许按值传递(而不是衰减)?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/50775127/

    有关c++ - 从编译器的角度来看,如何处理数组的引用,以及为什么不允许按值传递(而不是衰减)?的更多相关文章

    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 - 为什么 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返

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

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

    4. 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中的所有其他对象

    5. ruby-on-rails - 使用 rails 4 设计而不更新用户 - 2

      我将应用程序升级到Rails4,一切正常。我可以登录并转到我的编辑页面。也更新了观点。使用标准View时,用户会更新。但是当我添加例如字段:name时,它​​不会在表单中更新。使用devise3.1.1和gem'protected_attributes'我需要在设备或数据库上运行某种更新命令吗?我也搜索过这个地方,找到了许多不同的解决方案,但没有一个会更新我的用户字段。我没有添加任何自定义字段。 最佳答案 如果您想允许额外的参数,您可以在ApplicationController中使用beforefilter,因为Rails4将参数

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

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

    7. ruby-on-rails - Enumerator.new 如何处理已通过的 block ? - 2

      我在理解Enumerator.new方法的工作原理时遇到了一些困难。假设文档中的示例:fib=Enumerator.newdo|y|a=b=1loopdoy[1,1,2,3,5,8,13,21,34,55]循环中断条件在哪里,它如何知道循环应该迭代多少次(因为它没有任何明确的中断条件并且看起来像无限循环)? 最佳答案 Enumerator使用Fibers在内部。您的示例等效于:require'fiber'fiber=Fiber.newdoa=b=1loopdoFiber.yieldaa,b=b,a+bendend10.times.m

    8. ruby - 当使用::指定模块时,为什么 Ruby 不在更高范围内查找类? - 2

      我刚刚被困在这个问题上一段时间了。以这个基地为例:moduleTopclassTestendmoduleFooendend稍后,我可以通过这样做在Foo中定义扩展Test的类:moduleTopmoduleFooclassSomeTest但是,如果我尝试通过使用::指定模块来最小化缩进:moduleTop::FooclassFailure这失败了:NameError:uninitializedconstantTop::Foo::Test这是一个错误,还是仅仅是Ruby解析变量名的方式的逻辑结果? 最佳答案 Isthisabug,or

    9. ruby - 为什么人们使用 `Module.send(:prepend, …)` ? - 2

      我正在学习如何在我的Ruby代码中使用Module.prepend而不是alias_method_chain,我注意到有些人使用send调用它(example):ActionView::TemplateRenderer.send(:prepend,ActionViewTemplateRendererWithCurrentTemplate)而其他人直接调用它(example):ActionView::TemplateRenderer.prepend(ActionViewTemplateRendererWithCurrentTemplate)而且,虽然我还没有看到任何人使用这种风格,但我从

    10. ruby - 为什么 Ruby 的 each 迭代器先执行? - 2

      我在用Ruby执行简单任务时遇到了一件奇怪的事情。我只想用每个方法迭代字母表,但迭代在执行中先进行:alfawit=("a".."z")puts"That'sanalphabet:\n\n#{alfawit.each{|litera|putslitera}}"这段代码的结果是:(缩写)abc⋮xyzThat'sanalphabet:a..z知道为什么它会这样工作或者我做错了什么吗?提前致谢。 最佳答案 因为您的each调用被插入到在固定字符串之前执行的字符串文字中。此外,each返回一个Enumerable,实际上您甚至打印它。试试

    随机推荐