Godbolt链接:https://godbolt.org/g/Hv6MAL
typedef int cell;
cell y;
const cell *phys_addr = (const cell*)0x12340;
int main() {
for (int i = 0; i < 20; i++) {
for (int j = 0; j < 30; j++) {
for (int k = 0; k < 50; k++) {
const cell *subarray = (&phys_addr[i] + phys_addr[i]/sizeof(cell));
const cell *subsubarray = (&subarray[j] + subarray[j]/sizeof(cell));
y = subsubarray[k];
}
}
}
}
期待编译器将上述代码优化为类似于以下内容是很自然的:
int main() {
for (int i = 0; i < 20; i++) {
const cell *subarray = (&phys_addr[i] + phys_addr[i]/sizeof(cell));
for (int j = 0; j < 30; j++) {
const cell *subsubarray = (&subarray[j] + subarray[j]/sizeof(cell));
for (int k = 0; k < 50; k++) {
y = subsubarray[k];
}
}
}
}
但是由gcc 8.2以-O3 -m32
作为标志生成的程序集是:
push ebp
push edi
push esi
push ebx
sub esp, 8
mov eax, DWORD PTR phys_addr
mov DWORD PTR [esp], 0
mov DWORD PTR [esp+4], eax
mov ebp, eax
.L4:
xor esi, esi
.L3:
lea edi, [0+esi*4]
xor eax, eax
.L2:
mov edx, DWORD PTR [ebp+0]
mov ecx, DWORD PTR [esp+4]
shr edx, 2
add edx, DWORD PTR [esp]
lea ebx, [ecx+edx*4]
lea edx, [eax+esi]
add eax, 1
mov ecx, DWORD PTR [ebx+edi]
shr ecx, 2
add edx, ecx
mov edx, DWORD PTR [ebx+edx*4]
mov DWORD PTR y, edx
cmp eax, 50
jne .L2
add esi, 1
cmp esi, 30
jne .L3
add DWORD PTR [esp], 1
mov eax, DWORD PTR [esp]
add ebp, 4
cmp eax, 20
jne .L4
add esp, 8
xor eax, eax
pop ebx
pop esi
pop edi
pop ebp
ret
为什么编译器不将subarray
和subsubarray
计算移到内部循环之外?
volatile
做魔术我随机添加了volatile
,以防止DCE删除所有代码,然后以某种方式将循环不变量从内部循环中吊起。
int main() {
for (int i = 0; i < 20; i++) {
for (int j = 0; j < 30; j++) {
for (int k = 0; k < 50; k++) {
const cell *subarray = (&phys_addr[i] + phys_addr[i]/sizeof(cell));
const cell *subsubarray = (&subarray[j] + subarray[j]/sizeof(cell));
volatile cell y = subsubarray[k];
}
}
}
return 0;
}
这主要不是因为y
是局部变量,因为使用std::cout << subsubarray[k];
妨碍了优化。
由gcc 8.2使用-O3 -m32
作为上述代码的标志生成的程序集是:
main:
push ebp
push edi
xor edi, edi
push esi
push ebx
sub esp, 20
mov ebp, DWORD PTR phys_addr
.L4:
mov eax, DWORD PTR [ebp+0+edi*4]
xor ecx, ecx
shr eax, 2
add eax, edi
lea ebx, [ebp+0+eax*4]
lea esi, [ebx+200]
.L3:
mov edx, DWORD PTR [ebx+ecx*4]
mov DWORD PTR [esp], ecx
shr edx, 2
add edx, ecx
sal edx, 2
lea eax, [ebx+edx]
add edx, esi
.L2:
mov ecx, DWORD PTR [eax]
add eax, 4
mov DWORD PTR [esp+16], ecx
cmp edx, eax
jne .L2
mov ecx, DWORD PTR [esp]
add ecx, 1
cmp ecx, 30
jne .L3
add edi, 1
cmp edi, 20
jne .L4
add esp, 20
xor eax, eax
pop ebx
pop esi
pop edi
pop ebp
ret
循环不变式从内部循环中推出。随机volatile
做了什么以使GCC优化不变量?当 clang 6.0.0 时,优化不会发生。
答案 0 :(得分:2)
这与解决问题的随机挥发无关-问题更深。
您已经猜到问题确实与“ y”有关
检查此示例:
using System.Collections.ObjectModel;
using System.Diagnostics;
using Windows.UI.Xaml.Controls;
namespace PlexHelper
{
public sealed partial class MainPage : Page
{
public ObservableCollection<Show> Shows { get; } = new ObservableCollection<Show>();
public Show SelectedShow { get; set; }
public MainPage()
{
this.InitializeComponent();
Shows.Add(new Show("Show 1"));
Shows.Add(new Show("Show 2"));
Shows.Add(new Show("Show 3"));
SelectedShow = Shows[0];
}
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
Debug.WriteLine("now selected: " + SelectedShow.Name);
}
}
}
我使用除法技巧来避免硬优化(gcc可以评估所有循环并直接以纯赋值形式提供y;使用加,减或乘运算时,它也会展开最内层的循环-请玩弄godbolt,看看它如何外观)
现在反汇编如下: https://godbolt.org/g/R1EGSb
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace PlexHelper
{
public class Show : INotifyPropertyChanged
{
private string _name;
public string Name
{
get => _name;
set
{
if (_name != value)
{
_name = value;
OnPropertyChanged();
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
public Show(string name = "Default Show Name")
{
Name = name;
}
public void OnPropertyChanged([CallerMemberName] string propertyName = null) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
.L2是最内层的循环,因此代码看起来像预期的那样-子数组和子子数组已预先计算。
所以您可能想知道-为什么当“ y”是局部的时,一切正常,而全局不是。
要明确-不必在main中声明“ y”。可以像这样将其设置为静态
typedef int cell;
const cell *phys_addr = (const cell*)0x12340;
int main() {
cell y = 1;
for (int i = 0; i < 20; i++) {
for (int j = 0; j < 30; j++) {
for (int k = 0; k < 50; k++) {
const cell *subarray = (&phys_addr[i] + phys_addr[i]/sizeof(cell));
const cell *subsubarray = (&subarray[j] + subarray[j]/sizeof(cell));
y /= subsubarray[k];
}
}
}
return y;
}
或使用命名空间
main:
push ebp
push edi
push esi
push ebx
sub esp, 12
mov eax, DWORD PTR phys_addr
mov DWORD PTR [esp], 0
mov DWORD PTR [esp+4], eax
mov eax, 1
.L4:
mov esi, DWORD PTR [esp]
mov edi, DWORD PTR [esp+4]
mov edx, DWORD PTR [edi+esi*4]
mov DWORD PTR [esp+8], edx
shr edx, 2
add edx, esi
xor esi, esi
lea edi, [edi+edx*4]
lea ebp, [edi+200]
.L3:
mov ebx, DWORD PTR [edi+esi*4]
shr ebx, 2
add ebx, esi
sal ebx, 2
lea ecx, [edi+ebx]
add ebx, ebp
.L2:
cdq
idiv DWORD PTR [ecx]
add ecx, 4
cmp ebx, ecx
jne .L2
add esi, 1
cmp esi, 30
jne .L3
add DWORD PTR [esp], 1
mov edi, DWORD PTR [esp]
cmp edi, 20
jne .L4
add esp, 12
pop ebx
pop esi
pop edi
pop ebp
ret
phys_addr:
.long 74560
,然后将y称为wtf :: y;
还是很好。
所有内容都简化为别名。要查看它,首先将y更改为指针:
static cell y;
const cell * __restrict__ phys_addr = (const cell*)0x12340;
不再进行循环优化......
可以假定y和phys_addr重叠-写入y可能会修改某些存储单元,因此所有字典都必须使用最新数据进行计算(phys_addr中的const表示仅指针不应该修改内存,不是全局只读)。
但是,如果您“保证”这些地址不重叠,优化将返回。
namespace wtf{ cell y; }
const cell * __restrict__ phys_addr = (const cell*)0x12340;
TL; DR;
如果您使用的是指针编译器,则可能无法证明地址没有别名并且将使用安全路径。如果您100%确定他们没有使用 restrict 来告知这一事实。