(理论上的)编译器和不同的传递类型

时间:2014-01-06 16:22:21

标签: c++ assembly compiler-construction pass-by-reference compiler-optimization

新序言

我对构造函数理论知之甚少,这是一个理论问题,关于C ++的理论构造函数如何编译语言以及它如何编译非常相似的汇编程序(以及二进制代码),以及是否汇编程序在三个不同的方法中是相似的,每个方法都使用相同数量的参数但提供相同的功能(可能是一个打印两个整数值的简单方法?)。

每个方法都使用一对整数作为参数但以不同的方式:一个方法按值传递,另一个方法按引用传递,最后一个方法通过地址传递。由于引用传递和地址变量必须对提供的内存位置的实际值起作用,它们(除了访问值的代码之外)是否在编译版本中包含相同的代码?

public void Foo (int a, int b)
{
   std:cout << a << " " << b <<endl;
}

public void Bar (int* a, int* b)
{
  // (aside from dereferencing code) the same code as Foo
}

public void FooBar (int& a, int& b)
{
  // again, the same code (fundamentally) here
}

原始问题

假设我写了三个相同的方法“Foo”,“Bar”和“FooBar”。每个方法都采用相同数量的参数,并且所有方法都是无效的。

public void Foo (int a, int b)
{
  // some code here
}

public void Bar (int* a, int* b)
{
  // (aside from dereferencing code) the same code as Foo
}

public void FooBar (int& a, int& b)
{
  // again, the same code (fundamentally) here
}

假设这种语言的编译器完美运行,没有错误和完全优化。

一个完美的这样的系统中,所有三种方法的输出汇编和二进制代码是否不匹配?如果没有,它们之间会有很大差异吗?

澄清:假设这些方法极其微不足道,因为它们不会改变传入参数的值。

5 个答案:

答案 0 :(得分:2)

显然,如果代码确实修改了ab,则第一个和最后两个函数的语义完全不同;这显然会迫使编译器生成不同的代码;除此之外,如果这三个函数在给定程序中的每个环境中实际上都是相同的,那么编译器可以自由地将它们全部减少为单个函数,这要归功于“似乎”规则。

但是这个问题没有多大意义 - 或者至少,在“完美系统”(对于我来说不可能存在的东西)中,询问二进制代码生成(一个非常具体的实现细节)的具体差异是不是非常有意义。

要回到实际系统,首先我们必须考虑内联;如果函数是内联的,那么它们与父函数的其余代码混合在一起,并且每个扩展中的输出可能不同(可以有不同的寄存器分配,不同的指令混合以最大化管道利用率,......)< / p>

因此,比较应该是关于每个函数的“独立”输出。

我希望第二个和第三个扩展到几乎相同的完全相同的代码:引用是指针的语法糖,而且你不能拥有NULL引用的事实已被考虑到事实上*a*b可能在Bar没有检查的情况下被解除引用(这告诉优化器假设它们永远不会是NULL)。另外,我不知道任何C ++ ABI区分指针和引用。

至于Foo,它取决于很多因素:

  • 如果我们正在编译库,编译器就不能自由地做任何想做的事情,并且函数必须遵守某些ABI;因此,首先,参数实际上是最后两种情况中的指针和第一种情况中的值(具有不同的后果,取决于平台ABI);
  • 如果编译器不具备LTCG并且我们期望从其他模块使用这些函数(即函数未标记为static),这也可以成立;
  • 在最后两种情况下,为了生成相同的输出,可能需要编译器证明引用/指针指向不同的值以生成与Foo相同的输出;
  • 另外,它必须能够证明ab在整个功能中都没有改变;特别是,在每个外部(=非完全内联)函数调用之后,指向的对象可能已经改变;这些都可能是复杂的任务,如果程序由多个模块组成,它们可能还需要LTCG。

所以我实际期望发生的事情是:

  • 适用于独立版本Foo!= BarFooBar; Bar==FooBar;
  • 对于内联版本,编译器可能会有更简单的时间来确定将“BarFooBar”转换为Foo的相同语义的条件,当然生成的代码与不同函数的代码混合在一起这一事实将导致不同的程序集输出(可能很难理解子程序代码的开始/结束位置)。

答案 1 :(得分:2)

优化编译器很可能为Foo生成比BarFooBar更好的代码,除了相当简单的函数。

原因是对于Foo,编译器可能会假设ab的值在整个函数中是常量,除非对这些变量之一进行显式赋值。即使有,大多数现代编译器的中间表示都将表示为 new 变量的赋值,并且从赋值点开始简单地使用该变量而不是原始变量。

对于BarFooBar,这种推理只有在

的情况下才有效
  • 该函数不会调用其他非内联函数,因为任何此类函数都可能更改ab所指向的值

  • 该函数不会通过char*类型的指针修改任何内存,因为此类指针可能合法地指向任何类型的数据,包括int (谷歌针对完整独家新闻的“严格别名规则”)

BarFooBar可能会产生类似的代码,除非你处于一个神秘的平台上,ABI会在这个平台上使用不同的引用和指针。

答案 2 :(得分:1)

在完美系统中,是.. !!

然而,这种优化水平非常极端。 BarFooBar几乎与Foo相同,但事实上它们几乎相同会使编译器难以检测相似性,因为它基本上是在做差异生成的代码,然后必须弄清楚差异有多大。

如果你想要这种优化水平,那么你可能最好不要像这样编写代码

public void Foo(int a, int b)
{
  // Whatever
}

public void Bar (int* a, int* b)
{
  Foo(*a, *b);
}

public void FooBar (int& a, int& b)
{
  Foo(a, b);
}

现在,编译器可以选择将Foo内联到BarFooBar,这对编译器来说是一个相当简单的优化决策。

答案 3 :(得分:1)

假设C ++样式声明,除了调试信息之外,Bar和Foobar将是相同的,因为内部的引用是指针,只有访问样式使它们不受指针的影响。

对于第一个(Foo),如果不知道将来如何使用这些函数,则取决于你是否真的允许修改函数声明样式。如果他们的接口假设他们可以在指针下更改值(即使在极端条件下),则不允许仅传递值进行优化。但是,假设经常调用函数,编译器和/或运行时可以将其更改为没有指针的变量,并且具有直接值传递。 (但是,更有可能的是,将应用其他一些优化,例如全功能内联。)

答案 4 :(得分:1)

  

在一个完美的系统中,所有三种方法的输出汇编和二进制代码是否不匹配?

否,因为如果代码写入a和b,则Foo修改a和b的本地副本,而FooBar修改原始(调用者的副本)a和b的值。