(【理论】)编译器和不同的传递类型
(theoretical) Compilers and different pass-by types
新前言
我对构造函数理论不太了解,这是一个理论问题,关于类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"answers"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
}
假设该语言的编译器运行良好,没有任何错误,并且进行了充分优化。
在这样一个完美的系统中,输出的程序集和三种方法的二进制代码会不匹配吗?如果没有,它们之间会有巨大的差异吗?
澄清:假设这些方法非常琐碎,因为它们不会更改传入参数的值。
显然,如果代码确实修改了a
和b
,则前两个函数和后两个函数的语义完全不同;这显然会迫使编译器生成不同的代码;除此之外,如果在给定程序的每种情况下,这三个函数实际上都是相同的,那么编译器当然可以根据"好像"规则将所有函数简化为一个函数。
但这个问题没有多大意义——或者至少,询问"完美系统"(不可能存在的东西)中二进制代码生成(一个非常具体的实现细节)的具体差异对我来说并没有真正的意义。
为了回到真实的系统,首先我们必须考虑内联;如果函数是内联的,那么它们会与父函数的其余代码混合,并且每次扩展的输出可能不同(可以有不同的寄存器分配,不同的指令混合以最大限度地提高流水线利用率,…)
因此,比较应该是关于每个函数的"独立"输出。
我希望第二个和第三个扩展到几乎完全相同的代码:引用是指针的语法糖,而*a
和*b
可能在Bar
中没有检查的情况下被取消引用,这已经考虑到了不能有NULL
引用的事实(这告诉优化器假设它们永远不会是NULL
)。此外,我不知道有什么C++ABI可以区分指针和引用。
至于Foo
,它将取决于许多因素:
- 如果我们正在编译一个库,编译器不能随心所欲,函数必须遵守一些ABI;出于这个原因,首先,在最后两种情况下,参数实际上是指针,在第一种情况下是值(根据平台ABI的不同,结果会有所不同)
- 如果编译器不能LTCG,并且我们希望使用其他模块中的这些函数(即,这些函数没有标记为
static
),则这种情况也会发生 - 在最后两种情况下,为了生成相同的输出,编译器可能需要证明引用/指针指向不同的值,以生成与
Foo
相同的输出 - 还必须能够证明
a
和b
在整个函数中没有变化;特别是,在每次外部(=非完全内联)函数调用之后,指向的对象可能已经改变;这两者都可能是复杂的任务,而且,如果程序由几个模块组成,它们可能需要LTCG
所以我实际期望的是:
- 对于独立版本,
Foo!= Bar
和FooBar
;Bar==FooBar
- 对于内联版本,编译器可能会有更简单的时间来确定将
Bar
和FooBar
"转换"为Foo
的相同语义的条件,但当然,生成的代码与不同函数的代码混合的事实将导致不同的汇编输出(以至于可能很难理解子程序的代码从哪里开始/结束)
优化编译器很可能为Foo
生成比Bar
或FooBar
更好的代码,除了一些微不足道的函数。
原因是对于Foo
,编译器可能会假设a
和b
的值在整个函数中是恒定的,除非对其中一个变量进行了显式赋值。即使存在,大多数现代编译器的中间表示也会将这样的赋值表示为新变量,并从赋值开始简单地使用该变量而不是原始变量。
然而,对于Bar
和FooBar
,这种推理只有在
-
该函数不调用其他非内联函数,因为任何此类函数都可以更改
a
或b
所指向的值 -
该函数不会通过
char*
类型的指针修改任何内存,因为这样的指针可以合法地指向任何类型的数据,包括int
(谷歌对完整独家的"严格别名规则")
Bar
和FooBar
可能会生成类似的代码,除非您在某个神秘的平台上,ABI对引用和指针的处理方式不同。
在一个完美的系统中,是的。。!!
然而,这种级别的优化是相当极端的。Bar
和FooBar
与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
内联到Bar
和FooBar
中,这对于编译器来说是一个相当简单的优化决策。
假设C++风格的声明,Bar和Foobar除了调试信息外将是相同的,因为内部引用是指针,只有访问风格会从指针中分离它们。
对于第一个(Foo),这取决于如果不知道函数将来将如何使用,您是否真的允许修改函数声明样式。如果他们的接口假设他们可以更改指针下的值(即使在非常极端的条件下),则不允许只传递值的优化。但是,假设经常调用函数,编译器和/或运行时可以通过直接值传递将其更改为不带指针的变体。(但是,更有可能的是,还会应用一些其他优化,例如全函数内联。)
在一个完美的系统中,所有三种方法的输出程序集和二进制代码会不匹配吗?
否,因为如果代码写入a和b,那么Foo会修改a和b的本地副本,而FooBar则会修改原始(调用方的副本)a和b。
- ArduinoJson 6.15.2:JsonObject没有命名类型
- 防止主数据类型C++的隐式转换
- 大量序列中核苷酸类型的快速计数
- 如何从C++中的依赖类型中获得它所依赖的类型
- 有关插入适配器的错误。[错误]请求从 'back_insert_iterator<vector<>>' 类型转换为非标量类型
- 是否可以初始化不可复制类型的成员变量(或基类)
- 如何获取std::result_of函数的返回类型
- 从父命名空间重载类型
- 如果C++类在类方法中具有动态分配,但没有构造函数/析构函数或任何非静态成员,那么它仍然是POD类型吗
- 我想将一个对T类型的非常量左值引用绑定到一个T类型的临时值
- Openssl 1.1.1d无效使用不完整的类型"struct dsa_st"
- 访问者访问变体并返回不同类型时出错
- 在VS2010-VS2015下编译时,如何使用decltype作为较大类型表达式的LHS
- 处理小于cpu数据总线的数据类型.(c++转换为机器代码)
- C++ 雷神库 - 使用资源加载器类时出现问题(不命名类型)
- 模板元程序查找相似的连续类型名称
- 是否可以从int转换为enum类类型
- (【理论】)编译器和不同的传递类型
- 与模板类型相关的编译时错误发生在理论上不发生的地方
- 在C语言中,一组函数的名称可能因操作数类型的不同而不同,称为理论静态多态函数