C++ 为什么T*可以在寄存器中传递,但却是唯一的\u ptr<;T>;不能

C++ 为什么T*可以在寄存器中传递,但却是唯一的\u ptr<;T>;不能,c++,assembly,unique-ptr,calling-convention,abi,C++,Assembly,Unique Ptr,Calling Convention,Abi,我正在看钱德勒·卡拉斯在CppCon 2019的演讲: 在这篇文章中,他举了一个例子,说明他是如何惊讶于在int*上使用std::unique_ptr所产生的开销;该段大约在时间点17:25开始 你可以看一下他的示例代码片段对(godbolt.org)——事实上,编译器似乎不愿意将唯一的_ptr值传递到寄存器中,而实际上它只是一个地址,只在直接内存中传递 卡鲁思先生在27点左右提出的一个观点是,C++ abi需要通过值参数(一些但不是全部;也许非原始类型?非平凡构造类型)在内存中传递而不是登记

我正在看钱德勒·卡拉斯在CppCon 2019的演讲:

在这篇文章中,他举了一个例子,说明他是如何惊讶于在
int*
上使用
std::unique_ptr
所产生的开销;该段大约在时间点17:25开始

你可以看一下他的示例代码片段对(godbolt.org)——事实上,编译器似乎不愿意将唯一的_ptr值传递到寄存器中,而实际上它只是一个地址,只在直接内存中传递

卡鲁思先生在27点左右提出的一个观点是,C++ abi需要通过值参数(一些但不是全部;也许非原始类型?非平凡构造类型)在内存中传递而不是登记在寄存器中。 我的问题是:

  • 在某些平台上,这实际上是ABI要求吗?(哪个?)或者可能只是某些场景中的一些悲观情绪
  • 为什么ABI是这样的?也就是说,如果一个结构/类的字段适合于寄存器,甚至是单个寄存器,为什么我们不能在该寄存器中传递它呢
  • <> LI> C++标准委员会最近几年讨论过这一点,或者曾经?
    PS-为了不让这个问题没有代码:

    普通指针:

    void bar(int* ptr) noexcept;
    void baz(int* ptr) noexcept;
    
    void foo(int* ptr) noexcept {
        if (*ptr > 42) {
            bar(ptr); 
            *ptr = 42; 
        }
        baz(ptr);
    }
    
    唯一指针:

    using std::unique_ptr;
    void bar(int* ptr) noexcept;
    void baz(unique_ptr<int> ptr) noexcept;
    
    void foo(unique_ptr<int> ptr) noexcept {
        if (*ptr > 42) { 
            bar(ptr.get());
            *ptr = 42; 
        }
        baz(std::move(ptr));
    }
    
    使用std::unique\u ptr;
    无效条(int*ptr)不例外;
    void baz(唯一)无例外;
    void foo(唯一)无例外{
    如果(*ptr>42){
    bar(ptr.get());
    *ptr=42;
    }
    baz(std::move(ptr));
    }
    
  • 这实际上是一个ABI需求,或者可能只是某些场景中的一些悲观情绪
  • 一个例子是。此ABI用于64位x86兼容CPU(Linux x86_64体系结构)。以下是Solaris、Linux、FreeBSD、macOS、Windows Linux子系统:

    如果C++对象具有非平凡的拷贝构造函数或非平凡的 析构函数,它通过不可见引用传递(对象在 参数列表(由具有类整数的指针创建)

    不能创建具有非平凡复制构造函数或非平凡析构函数的对象 按值传递,因为此类对象必须具有定义良好的地址。类似的问题也适用 从函数返回对象时

    注意,只有2个通用寄存器可用于传递1个具有普通复制构造函数和普通析构函数的对象,即只有
    sizeof
    不大于16的对象的值才能在寄存器中传递。有关调用约定的详细处理,请参见§7.1传递和返回对象。在寄存器中传递SIMD类型有单独的调用约定

    其他CPU架构有不同的ABI


    除MSVC外,大多数编译器还遵循以下几点:

    如果参数类型对于调用来说是非平凡的,则调用方必须为临时文件分配空间,并通过引用传递该临时文件

    在以下情况下,就调用而言,类型被认为是非平凡的:

    • 它有一个非平凡的复制构造函数、移动构造函数或析构函数,或
    • 将删除其所有复制和移动构造函数
    应用于类类型的此定义旨在补充[class.temporary]p3中的定义,即在传递或返回类型时允许使用额外临时的类型。根据基本C ABI的规则(例如,在寄存器中),将传递和返回一个对于ABI而言微不足道的类型;这通常会产生执行该类型的普通副本的效果


  • 为什么ABI是这样的?也就是说,如果一个结构/类的字段适合于寄存器,甚至是单个寄存器,为什么我们不能在该寄存器中传递它呢
  • 这是一个实现细节,但在堆栈展开期间处理异常时,具有自动存储持续时间的被销毁对象必须相对于函数堆栈帧可寻址,因为此时寄存器已被删除。堆栈展开代码需要对象的地址来调用其析构函数,但寄存器中的对象没有地址

    迂腐地说:

    对象在其构造期间([class.cdtor])、整个生命周期和销毁期间占据存储区域

    如果没有分配可寻址的存储空间,则对象不能存在于C++中,因为 当需要一个对象的地址,并且该地址包含一个保存在寄存器中的简单复制构造函数时,编译器只需将该对象存储到内存中并获取该地址即可。另一方面,如果复制构造函数是非平凡的,则编译器不能将其存储到内存中,而是需要调用复制构造函数,该构造函数接受引用,因此需要寄存器中对象的地址。调用约定可能无法确定复制构造函数是否内联在被调用方中

    考虑这一点的另一种方式是,对于普通的可复制类型,编译器在寄存器中传输对象的值,如果需要,可以通过普通内存存储从寄存器中恢复对象。例如:

    void f(long*);
    void g(long a) { f(&a); }
    
    在带有System V的x86_64上,ABI编译为:

    g(long):                             // Argument a is in rdi.
            push    rax                  // Align stack, faster sub rsp, 8.
            mov     qword ptr [rsp], rdi // Store the value of a in rdi into the stack to create an object.
            mov     rdi, rsp             // Load the address of the object on the stack into rdi.
            call    f(long*)             // Call f with the address in rdi.
            pop     rax                  // Faster add rsp, 8.
            ret                          // The destructor of the stack object is trivial, no code to emit.
    

    钱德勒·卡鲁斯(Chandler Carrush)在他发人深省的讲话中指出,为了实施能够改善现状的破坏性举措,可能有必要(除其他外)对ABI进行突破性的改变。在我看来,如果使用新ABI的函数显式地选择具有新的不同链接,例如在
    extern“C++20”{}
    块中声明它们(可能在用于迁移现有API的新内联命名空间中),则ABI更改可能不会中断。因此,只有针对具有新链接的新函数声明编译的代码才能使用新ABI

    注意
    struct Foo { int bar; };
    Foo test(Foo byval) { return byval; }
    
    test(Foo):
            mov     eax, edi
            ret
    
    struct Foo2 {
        int bar;
        ~Foo2() {  }
    };
    
    Foo2 test(Foo2 byval) { return byval; }
    
    test(Foo2):
            mov     edx, DWORD PTR [rsi]
            mov     rax, rdi
            mov     DWORD PTR [rdi], edx
            ret
    
    void callee(int &i) {
      something(&i);
    }
    
    void caller() {
      int i;
      callee(i);
      something(&i);
    }
    
    struct file_handler { // don't use that class!
        file_handler () { this->fileno = -1; }
        file_handler (int f) { this->fileno = f; }
        file_handler (const file_handler& rhs) {
            if (this->fileno != -1)
                this->fileno = dup(rhs.fileno);
            else
                this->fileno = -1;
        }
        ~file_handler () {
            if (this->fileno != -1)
                close(this->fileno); 
        }
        file_handler &operator= (const file_handler& rhs);
    };