内存分配优化
Memory assignments optimizations
众所周知,编译器可以混合赋值顺序以优化执行,
所以-
a=b;
c=d;
可以实际执行
c=d;
a=b;
但是使用以下代码:
a=b;
x=a;
func(x);
调用 func(x)
时,x
必须先包含 b
,否则结果不可预知。
现在,下面的代码呢:
int *addr1 = some_addr;
int *addr2 = (int *)0xf00;
/* The following applies:
* some_other_addr >= some_addr
*/
for (addr1; addr1 < some_other_addr; addr1++)
{
*addr1 += 1;
}
*addr2 *= 8;
当addr2
指向for
循环范围内的地址时,我们需要知道是否承诺*addr2
在乘法前自增8,好像没有,并且一些优化步骤在 for
循环之前放置 *addr2 *= 8;
,*addr2
的结果将与没有优化的情况下执行的结果不同。
如果 some_addr
和 some_other_addr
在作用域中定义并且它们作为参数传递,答案会有所不同吗?因为在第一种情况下,编译器很容易知道 *addr2
在 for
循环的范围内,而在第二种情况下则不是那么明显。
此外,如果我们从汇编的角度来看,让我们以 reset_handler
段初始化的示例 reset_handler
代码片段为例:
ldr r1, =__BSS_SIZE__
cmp r1, #0
beq FINISHED_LABEL
ldr r0, =__BSS_START__
ldr r2, =0
LOOP_LABEL:
str r2, [r0]
add r0, r4
subs r1, r4
bne LOOP_LABEL
如果此代码之后的下一条指令(在 FINISHED_LABEL
)从 bss 范围内的地址加载一个值(ldr
),是否承诺内容在以下位置有效(0)那个时候?
编译器必须做的才能做到这一点称为 "alias analysis"。
如果编译器可以证明 addr2
不在 addr1
循环的范围内,它可以对其重新排序或在整个循环中将 *addr2
保留在寄存器中。
对于像 for(...; addr1++) { *addr1 += *addr2; }
这样的情况,这是一个非常有用的优化,可以避免每次都重新加载 addr2
,这也是 the restrict
keyword 存在的原因之一。
如果输入可能重叠,编译器可以(并且做)发出检查重叠的代码 并在没有重叠的情况下运行优化的(例如 auto-vectorized)循环,或者在有重叠的情况下运行安全循环。
如果编译器不能证明转换将给出与 C 抽象机相同的最终结果,它就不能进行转换。 (我说 "final" 是因为存储到内存的顺序不是可观察结果的一部分,除非您使用 std::atomic
。所以 compile-time 转换不允许破坏 single-threaded 代码,与 out-of-order CPU 所做的非常相似:为单个线程提供一切按程序顺序发生的错觉。)
as-if 规则只允许在 所有 情况下工作但不会导致 UB 的优化,包括模糊unsigned size = 0xffffffff
之类的东西通常会导致编译器无法进行您希望的优化,除非您调整源代码。
UB 是允许某些优化的关键(比如不在循环内重做 sign-extension 有符号数组索引)。参见
每个 C 程序员都应了解的未定义行为#1/3.
众所周知,编译器可以混合赋值顺序以优化执行, 所以-
a=b;
c=d;
可以实际执行
c=d;
a=b;
但是使用以下代码:
a=b;
x=a;
func(x);
调用 func(x)
时,x
必须先包含 b
,否则结果不可预知。
现在,下面的代码呢:
int *addr1 = some_addr;
int *addr2 = (int *)0xf00;
/* The following applies:
* some_other_addr >= some_addr
*/
for (addr1; addr1 < some_other_addr; addr1++)
{
*addr1 += 1;
}
*addr2 *= 8;
当addr2
指向for
循环范围内的地址时,我们需要知道是否承诺*addr2
在乘法前自增8,好像没有,并且一些优化步骤在 for
循环之前放置 *addr2 *= 8;
,*addr2
的结果将与没有优化的情况下执行的结果不同。
如果 some_addr
和 some_other_addr
在作用域中定义并且它们作为参数传递,答案会有所不同吗?因为在第一种情况下,编译器很容易知道 *addr2
在 for
循环的范围内,而在第二种情况下则不是那么明显。
此外,如果我们从汇编的角度来看,让我们以 reset_handler
段初始化的示例 reset_handler
代码片段为例:
ldr r1, =__BSS_SIZE__
cmp r1, #0
beq FINISHED_LABEL
ldr r0, =__BSS_START__
ldr r2, =0
LOOP_LABEL:
str r2, [r0]
add r0, r4
subs r1, r4
bne LOOP_LABEL
如果此代码之后的下一条指令(在 FINISHED_LABEL
)从 bss 范围内的地址加载一个值(ldr
),是否承诺内容在以下位置有效(0)那个时候?
编译器必须做的才能做到这一点称为 "alias analysis"。
如果编译器可以证明 addr2
不在 addr1
循环的范围内,它可以对其重新排序或在整个循环中将 *addr2
保留在寄存器中。
对于像 for(...; addr1++) { *addr1 += *addr2; }
这样的情况,这是一个非常有用的优化,可以避免每次都重新加载 addr2
,这也是 the restrict
keyword 存在的原因之一。
如果输入可能重叠,编译器可以(并且做)发出检查重叠的代码 并在没有重叠的情况下运行优化的(例如 auto-vectorized)循环,或者在有重叠的情况下运行安全循环。
如果编译器不能证明转换将给出与 C 抽象机相同的最终结果,它就不能进行转换。 (我说 "final" 是因为存储到内存的顺序不是可观察结果的一部分,除非您使用 std::atomic
。所以 compile-time 转换不允许破坏 single-threaded 代码,与 out-of-order CPU 所做的非常相似:为单个线程提供一切按程序顺序发生的错觉。)
as-if 规则只允许在 所有 情况下工作但不会导致 UB 的优化,包括模糊unsigned size = 0xffffffff
之类的东西通常会导致编译器无法进行您希望的优化,除非您调整源代码。
UB 是允许某些优化的关键(比如不在循环内重做 sign-extension 有符号数组索引)。参见 每个 C 程序员都应了解的未定义行为#1/3.