【问题标题】:Can static_cast to same type introduce runtime overhead?static_cast 可以引入相同类型的运行时开销吗?
【发布时间】:2013-10-07 01:27:00
【问题描述】:

我有一个结构模板,它采用两种类型(TS),并且在某些时候使用static_cast 从一种类型转换为另一种类型。很多时候TS 是同一个类型。

设置的简化示例:

template <typename T, typename S = T>
struct foo
{
  void bar(T val)
  {
    /* ... */
    some_other_function(static_cast<S>(val));
    /* ... */
  }
};

如果ST 是同一个类,static_cast 是否或是否会引入额外的开销,或者它是一个始终会被忽略的空操作?

如果确实引入了开销,是否有一个简单的模板元编程技巧仅在需要时执行static_cast,还是我需要创建部分专业化来处理T == S 情况?如果可能,我宁愿避免整个foo 模板的部分特化。

【问题讨论】:

  • 不会有任何开销,如果它们是相同的类型,它将是空操作(类似于int a = 5; int b = static_cast&lt;int&gt;(a);
  • 即使对于像std::string 这样的复杂类型也是如此吗?例如,如果some_other_function 的参数是const std::string&amp;,是否保证传递对val 的引用而不是val 的临时副本?
  • @JonathanPotter:不幸的是,这只是错误的。我检查了g++ 5.3、g++ 6.1和clang++ 7.3,它们都调用了复制构造函数来为static_cast构建一个临时对象到相同的类型。
  • @Kundor 在上面(和下面)的例子中,复制构造函数将来自调用some_other_functionbar 等并按值传递。不是来自static_cast 本身。
  • @JonathanPotter: 不。t2 = static_cast&lt;T&gt;(t1) 首先调用一个复制构造函数(以创建一个未命名的临时对象),然后调用赋值运算符,当一切都是 T 类型时。它与t2 = t1 不同(它只是调用赋值运算符。)事实上,标准规定static_cast&lt;T&gt;(t1) 与声明T temp(t1) 相同,(其中temp 实际上是一个未命名的变量)并且表达式具有值temp

标签: c++ overhead static-cast


【解决方案1】:

是的,可以。

这是一个例子:

struct A {
  A( A const& ) {
    std::cout << "expensive copy\n";
  }
};

template<typename T>
void noop( T const& ) {}
template <typename T, typename S = T>
void bar(T val)
{
  noop(static_cast<S>(val));
}
template <typename T>
void bar2(T val)
{
  noop(val);
}
int main() {
  std::cout << "start\n";
  A a;
  std::cout << "bar2\n";
  bar2(a); // one expensive copy
  std::cout << "bar\n";
  bar(a); // two expensive copies
  std::cout << "done";
}

基本上,static_cast 可以诱导调用复制构造函数。

对于某些类型(如int),复制构造函数基本上是免费的,编译器可以消除它。

对于其他类型,它不能。在这种情况下,复制省略也是不合法的:如果您的复制构造函数有副作用,或者编译器无法证明它没有副作用(如果复制构造函数不平凡,则很常见),它将被调用。

【讨论】:

  • 在我看来,复制省略在这里是合法的,因为 C++ 标准特别允许将任何复制省略为相同类型,即使复制构造函数有副作用。不幸的是,我检查过的编译器不会将 static_cast 的副本删除为相同的类型。他们在T t2 = static_cast&lt;T&gt;(t1); 中省略了一份副本,因此它与T t2(t1); 相同。但是分配t2 = static_cast&lt;T&gt;(t1)T temp(t1); t2 = temp 相同。
  • @Kundor 引用?我知道省略是合法的情况;我不知道它特别适用于static_cast。 Elision 关乎生命周期,是的,它可以消除副作用,但这不是我认为它不可消除的原因。
  • 我通过Wikipedia description“当类类型的临时对象被复制到相同类型的对象时。”但是我错了,因为这是一个命名对象到一个临时的副本,所以它不适用。
【解决方案2】:

为了补充Yakk's answer,我决定发布一些程序集来确认这一点。我使用std::string 作为测试类型。

foo&lt;std::string&gt;.bar() - 没有强制转换

pushq   %rbp
movq    %rsp, %rbp
subq    $32, %rsp
movq    %rcx, 16(%rbp)
movq    %rdx, 24(%rbp)
movq    24(%rbp), %rax
movq    %rax, %rcx
call    _Z19some_other_functionRKSs
nop
addq    $32, %rsp
popq    %rbp
ret

foo&lt;std::string&gt;.bar() - static_cast&lt;T&gt;()

pushq   %rbp
pushq   %rbx
subq    $56, %rsp
leaq    128(%rsp), %rbp
movq    %rcx, -48(%rbp)
movq    %rdx, -40(%rbp)
movq    -40(%rbp), %rdx
leaq    -96(%rbp), %rax
movq    %rax, %rcx
call    _ZNSsC1ERKSs     // std::string.string()
leaq    -96(%rbp), %rax
movq    %rax, %rcx
call    _Z19some_other_functionRKSs
leaq    -96(%rbp), %rax
movq    %rax, %rcx
call    _ZNSsD1Ev    // std::string.~string()
jmp .L12
movq    %rax, %rbx
leaq    -96(%rbp), %rax
movq    %rax, %rcx
call    _ZNSsD1Ev    // std::string.~string()
movq    %rbx, %rax
movq    %rax, %rcx
call    _Unwind_Resume
nop
.L12:
addq    $56, %rsp
popq    %rbx
popq    %rbp
ret


此代码仅使用-O0 生成。任何优化级别都会平衡这两种情况。

【讨论】:

  • 一般来说,讨论没有优化生成的程序集的性能问题是没有意义的。
  • @MatteoItalia 是的。我仍然认为将两者进行比较会很有趣。
猜你喜欢
  • 2015-11-25
  • 1970-01-01
  • 2012-12-28
  • 2016-10-16
  • 1970-01-01
  • 2019-08-03
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多