CSE将来自相同内存状态的负载统一。在计算密集型代码中,例如加密/md5中的通用代码,这些负载可能具有较长的生命周期,导致加载的值溢出到栈上。这是很愚蠢的:我们已经拥有了内存中的值,所以可以根据需要从那里重新加载它,而无需使用堆栈空间。
也许我们可以以某种方式将此类负载标记为可再材料化,这样寄存器分配器就知道它可以重新发出加载而不是溢出。这里有一些不太明显的细节,比如如果我们需要重新计算加载的地址,以及地址计算是否比溢出加载的值更昂贵。
但是我们目前为加密/md5的通用块生成了一些非常愚蠢的代码。
要查看这一点,请将https://gist.github.com/josharian/42e66bf022f32da3da0a9b1bdf0a974b插入md5代码中,然后观察禁用CSE对加载的影响,例如通过向cmpVal代码添加以下内容:
if len(v.Args) > 0 && v.Args[len(v.Args)-1].Type.IsMemory() {
return lt2Cmp(v.ID < w.ID)
}
生成的代码从一堆溢出和恢复操作转变成了简单直接的计算。
我发现没有办法调整代码来防止这种CSE。(发出无关的写入会导致其他值溢出,抵消了防止CSE的好处。)
cc @randall77
3条答案
按热度按时间2q5ifsrm1#
保持负载地址存活更长时间的需求确实给这个问题带来了困扰。我们需要进行一些分析,看看这是否值得。
到目前为止,我们一直保持这样的不变性:如果你执行以下操作:
那么我们保证在使用
x
时,它仍然是非零的。在重新加载和竞争的情况下,这一点不再成立。这听起来可能不是什么大问题,因为如果有竞争,我们可以做任何事情。但事实并非如此简单 - 在某些竞争中,我们不能只是说“你输了”。例如,
sync/mutex.go:tryLock
中的代码仅依赖于old := m.state
仅发生一次。如果old
从m.state
重新加载,就会出现糟糕的事情。有趣的相关问题:#48222
ovfsdjhp2#
在这种情况下,我们只会在程序已经包含一个加载的情况下重新加载——我们正在让 regalloc 撤销 CSE 的工作。或者,我们可以尝试猜测何时 CSE 一个加载会比留着它更糟糕。
我可以告诉你,拒绝对任何具有最终内存参数的内容进行 CSE 是一个坏主意。 :)
yiytaume3#
这确实是在#48222中遇到的问题——没有办法区分两个被CSE(Common Subexpression Elimination,公共子表达式消除)的程序,在这种情况下,将其拆分回来是可以接受的,而我们想要将一个包含单个加载的程序拆分成两个加载的程序,在这种情况下,拆分可能会出现问题。如果想要拆分负载,我们必须以某种方式跟踪其原始负载。
我同意,在认为它可能引入过多寄存器压力的情况下,不进行CSE负载可能是更容易的事情。但“容易”在这里是一个相对的概念,两种解决方案似乎都很困难。