Swift实现benchmark::DoNotOptimize类似功能及Swift6编译优化疑问
问题描述
我在Swift中做微基准测试,用的是package-benchmark工具,它提供的blackHole函数能防止死代码消除。但我需要进一步阻止公共子表达式消除(common subexpression elimination)和循环不变代码外提(loop-invariant code motion),于是基于blackHole实现了assumePointeeIsClobbered函数。测试发现一个差异:Swift 6.0.3会把代码里的sqrt调用优化掉,但Swift 5.10不会。
我有两个问题:
- Swift 6.0.3的这个行为是不是bug?如果不是,编译器为什么允许这种优化?
- 有没有更优的
assumePointeeIsClobbered实现方式?
问题1:这不是编译器bug,是优化策略的升级
Swift 6的优化器在纯函数识别和冗余计算消除上做了更激进的改进。sqrt是典型的纯函数——输入相同则输出必然相同,且无任何副作用。如果你的assumePointeeIsClobbered实现没有向编译器传递足够强的「指针指向的内存可能被意外修改」的信号,Swift 6的优化器会判断sqrt的输入值未发生变化,进而把重复的sqrt调用或循环内的sqrt调用直接优化掉。
举个例子,如果你的assumePointeeIsClobbered只是简单调用blackHole(ptr),编译器可能会识别出这并没有实际修改内存,因此不会阻止对纯函数的冗余计算消除。而Swift 5.10的优化器还没做到这么精细的分析,所以保留了sqrt调用。
问题2:更可靠的assumePointeeIsClobbered实现
要让编译器彻底放弃对指针指向内存的不变性假设,你需要构造一个编译器无法预测副作用的操作。以下是几种更有效的实现方式:
方式1:结合CompilerAssume和不内联函数
利用Swift的CompilerAssume告诉编译器「某个条件可能成立」,配合不内联的函数让优化器无法看穿内部逻辑:
@inline(never) func assumePointeeIsClobbered<T>(_ ptr: UnsafeMutablePointer<T>) { // 强制编译器认为指针指向的值可能被修改 Swift.CompilerAssume(ptr.pointee != ptr.pointee) }
CompilerAssume会让编译器假设括号内的表达式可能为真,而ptr.pointee != ptr.pointee只有在指针指向的值被意外修改时才会成立,这样编译器就不敢再假设指针指向的值不变。
方式2:引入无意义的内存读写操作
通过对指针指向的内存做一个编译器无法优化掉的读写操作(即使实际不改变值):
@inline(never) func assumePointeeIsClobbered<T>(_ ptr: UnsafeMutablePointer<T>) { let temp = ptr.pointee ptr.pointee = temp // 用blackHole确保临时变量不被优化 blackHole(temp) }
这个操作看似无意义,但编译器无法确定是否有其他线程或外部代码修改了内存,因此会放弃对该指针指向值的不变性假设。
方式3:使用RawPointer的opaque操作
将指针转换为原始指针,做一个编译器无法追踪的操作:
@inline(never) func assumePointeeIsClobbered<T>(_ ptr: UnsafeMutablePointer<T>) { let rawPtr = UnsafeMutableRawPointer(ptr) // 读取一个字节再写回去,让编译器认为内存可能被修改 let byte = rawPtr.load(as: UInt8.self) rawPtr.storeBytes(of: byte, as: UInt8.self) blackHole(byte) }
这种方式利用原始指针的操作让优化器无法推断内存的实际状态,从而阻止相关的优化。
内容的提问来源于stack exchange,提问作者loonatick

