【问题标题】:Is an unnamed parameter actually passed during a function call?在函数调用期间是否实际传递了未命名的参数?
【发布时间】:2011-08-29 14:58:40
【问题描述】:
template <typename TAG>
fn(int left, TAG, int right)
{
}

fn(0, some_type_tag(), 1);
/* or */
fn(0,int(), 1); // where the primitive, int, is not empty.

编辑:这个问题有两种观点。

  1. 函数声明与定义。声明可能不会命名参数,但声明可能会命名。这不是我们感兴趣的观点。
  2. 模板视角,特别是在元编程中。有问题的参数是用于从特征中提取元结构的标签。这就是为什么参数没有命名的原因,我只关心编译时的信息——标签的类型。

/编辑

我的标签通常是空结构,但是在我的代码的某些部分,它们是原始类型的 typedef。所以,我很想知道现代编译器是否真的会传递参数。这有两个方面。

  1. 调整堆栈大小,考虑未命名参数类型的大小。
  2. 实际上是用传入的值构造堆栈。

让我们继续使用 gcc 4.5 和 msvc 2008+

【问题讨论】:

  • 结果会因编译器和优化级别而异。您必须尝试一下。
  • 我每天有 10 个这样的问题,如果我对所有事情都进行试验,我将无法完成任何工作 ;),我已经用我感兴趣的两个编译器对问题进行了限定。跨度>
  • @Hassan:我想你会在写一个简单的测试而不是写整个问题之前结束:)
  • 寄存器中传入的参数呢?
  • @diego,这可能是真的(但我对此表示怀疑),但即使是这样,当我忘记答案时,我将来也不会有一个很好的参考点。

标签: c++ micro-optimization


【解决方案1】:

C++ 有单独的翻译。由于参数可以在声明中命名,但不能在函数定义中命名,反之亦然,因此编译器通常无法知道省略函数参数是否安全。当它们都在同一个翻译单元中时,所有内容都可以内联,参数名称与优化完全无关。

[已添加]

单独的翻译可能对这种特定情况无关紧要,但是添加这种优化的编译器构建器必须注意。如果它破坏了完全有效的代码,他们不会进行这样的优化。

对于模板,模板函数的类型必须与非模板函数的类型相同,否则无法获取其地址并将其分配给函数指针。同样,您必须考虑单独的翻译。仅仅因为你没有在这个 TU 中使用foo&lt;int&gt; 的地址,并不意味着你不会在另一个 TU 中。

【讨论】:

  • 如果获取地址,编译器会生成一个函数调用。但是,除非必须,否则它不会(或不应该)使用这个新功能。这仍然使我的用例 =D 正在审查的函数类。所以也许我们可以假设,如果一个地址被使用,编译器会创建一个函数调用,同时考虑标签参数。
  • 这仍然存在一个问题:如果 one 调用者将参数考虑在内,则被调用者必须,这反过来意味着 所有 调用者必须。
【解决方案2】:

其实这是一个很有趣的问题。

首先,请注意,我们使用的是命令式语言,这意味着当您要求某些东西(甚至是无用的,例如构造一个未使用的对象)时,编译器需要遵守,除非它可以提出等价的形式。基本上,如果它可以证明这样做不会改变程序的含义,它可以省略参数。

当你编写一个函数调用时,可能会发生两件事(最终):

  • 要么是内联的
  • call 实际发出

如果是内联,则不传递参数,这实际上意味着如果编译器可以证明所涉及的构造函数和析构函数不执行,则可以删除(甚至不构建)未使用的对象任何重要的工作。它适用于标签结构。

发出调用时,会使用特定的调用约定发出调用。每个编译器都有自己的一组调用约定,它们指定如何传递各种参数(this 指针等),通常会尝试利用可用的寄存器。

由于只用函数的声明来确定调用约定(单独编译模型),那么就需要实际传递对象...

但是,如果我们谈论的是一个空结构,没有方法也没有状态,那么这只是一些未初始化的内存。它应该不会花费太多,但它确实需要堆栈空间(至少,保留它)。

使用 llvm 试用的演示:

struct tag {};

inline int useless(int i, tag) { return i; }

void use(tag);

int main() {
  use(tag());
  return useless(0, tag());
}

给予:

%struct.tag = type <{ i8 }>

define i32 @main() {
entry:
  ; allocate space on the stack for `tag`
  %0 = alloca %struct.tag, align 8                ; <%struct.tag*> [#uses=2]

  ; get %0 address
  %1 = getelementptr inbounds %struct.tag* %0, i64 0, i32 0 ; <i8*> [#uses=1]

  ; 0 initialize the space used for %0
  store i8 0, i8* %1, align 8

  ; call the use function and pass %0 by value
  call void @_Z3use3tag(%struct.tag* byval %0)
  ret i32 0
}

declare void @_Z3use3tag(%struct.tag* byval)

注意:

  • 如何删除对 useless 的调用,并且没有为其构建任何参数
  • 怎么调用use不能去掉,所以临时分配空间(希望新版本不要0-初始化内存)

【讨论】:

  • 我一直在寻找的风格、权威和语气恰到好处:D
  • @Hassan:我学的跟你一样多,而且我也知道注意力不集中,所以绝对权威^^
【解决方案3】:

参数是否命名对函数签名没有影响,编译器应该将其传入。考虑到函数声明中未命名的参数可能在定义中命名。

现在,在上述模板的特殊情况下,编译器可能会内联代码,在这种情况下不会传递任何参数,并且未命名的参数将不起作用。

如果你想要做的是标记以解析不同的重载,你总是可以回退到一个指针,这样即使它被传入,成本也会最小。

【讨论】:

  • 感谢我确实对模板透视图感兴趣。我已经对问题进行了进一步的限定以反映这一点。通常,由于我可能使用原始类型而不是空结构作为标记,因此大小开销不应该那么糟糕,如果我使用指针,我将始终为 64 位代码支付 8 字节开销:D。尽管我猜 8 字节的调用堆栈空间实际上对性能没有影响。
  • @Hassan Syed:如果您使用 64 位,调用约定可能会大量使用寄存器,这反过来意味着任何适合寄存器的成本将是重置它的成本(在非内联情况下)
  • 我不能真正同意指针参数,因为空结构(sizeof 1)小于指针(sizeof 4 或 8)。即使是原始类型 typedef 也应该更小。
  • @Xeo:很公平。如果您要使用空结构(或其他小类型),那么指针不会提供任何优势,只有当对象的大小更大时才会有帮助,这不是正在讨论的情况,因此我应该有让自己保持安静:)
【解决方案4】:

好问题,但您必须尝试使用​​您的编译器。理论上,如果不使用参数,则不必在堆栈中分配。但是,调用者必须知道如何调用它,所以我的猜测是该元素实际上是在堆栈中分配的。

【讨论】:

    猜你喜欢
    • 2010-09-20
    • 1970-01-01
    • 2013-01-15
    • 2014-03-07
    • 1970-01-01
    • 2020-05-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多