如何禁用V8的优化编译器

10
我正在编写一个常数时间字符串比较函数(用于node.js),并希望禁用V8的优化编译器,仅针对此单个函数;使用命令行标志不可行。
我知道使用with{}(或try/catch)块将立即禁用优化编译器,但我担心这个“特性”(漏洞)将在未来版本中被修复。
是否有一种不可变(且记录在文档中)的方法来禁用V8的优化编译器?
示例函数:
function constantTimeStringCompare( a, b ) {
    // By adding a `with` block here, we disable v8's optimizing compiler.
    // Using Object.create(null) ensures we don't have any object prototype properties getting in our way.our way.
    with ( Object.create( null ) ){
        var valid = true,
            length = Math.max( a.length, b.length );
        while ( length-- ) {
            valid &= a.charCodeAt( length ) === b.charCodeAt( length );
        }
        // returns true if valid == 1, false if valid == 0
        return !!valid;
    }
}

这里有一个只是为了好玩的性能测试


1
@DavidMurdoch,你的第一个问题是一个非常人为的情境。通常来说,运行时函数的“价值”是在给定输入大小 n 的情况下执行给定操作的次数。在你的情况下,这个操作将是字符比较。由于这将是更长字符串的长度,所以 a 是常量并不重要,因为对于所有比 a 更长的字符串,该函数的值将是 b.length。(由于存在无限数量的比 a 更长的字符串和有限数量的比 a 更短的字符串,我们可以在分析中忽略较短的字符串。) - millimoose
1
@DavidMurdoch 另外,对于算法复杂度,通常考虑渐近复杂度。在这里可以忽略检查引用相等的快捷方式。我现在能记起来的另一个是检查长度是否相等,这是一个常数时间检查,假设字符串长度事先已知。(就像在Javascript中一样。)这意味着使用它的函数对于不同长度的输入将是常数时间,对于相等长度的输入将是线性时间。因此,在这种情况下,省略这些检查将保证您的函数对于所有字符串都是线性时间 - millimoose
1
@DavidMurdoch 对于大多数字符串对来说,使用这些快捷方式在理论上是常数时间,而对于所有字符串对的一个无穷小子集则是线性的。实际上,这取决于您的输入范围。即使没有这些快捷方式,正如您所注意到的那样,由于.lengthmax()的开销,该函数也不会完全线性。 - millimoose
1
@DavidMurdoch 是的。这也是不正确的。 你的函数确定了a是否为b的前缀。(为了使这个比较成为常数时间,实际上你必须实现我提到的“优化”.) 同时将所有操作计入计数器是没有意义的。通常情况下,你会单独计算一个或几个“代表性”的操作实例。读取或写入输入元素(在你的情况下是字符)很常见,输入元素之间的比较也很常见。你可以安全地忽略常量开销。 - millimoose
1
@DavidMurdoch 实际上,读取字符串结尾之后的内容似乎明显变慢:http://jsperf.com/reading-past-the-end-of-a-string。因此,对于比`a`短的字符串,`b`的长度仍然很重要,所以你的函数实际上并不是恒定时间。(尽管从理论上讲,与所有可能的字符串集相比,这样的字符串并不多。) - millimoose
显示剩余19条评论
2个回答

14

如果您想要一个可靠的方法来做到这一点,您需要使用 --allow-natives-syntax 标志运行Node,并调用以下内容:

%NeverOptimizeFunction(constantTimeStringCompare);
请注意,在调用constantTimeStringCompare之前应该调用此函数,如果该函数已经被优化,则会违反断言。
否则,使用with语句是最好的选择,因为使其可优化是绝对的疯狂行为,而支持try/catch则是合理的。但这不会影响您的代码,以下内容就足够了:
function constantTimeStringCompare( a, b ) {
    with({});

    var valid = true,
        length = Math.max( a.length, b.length );
    while ( length-- ) {
        valid &= a.charCodeAt( length ) === b.charCodeAt( length );
    }
    // returns true if valid == 1, false if valid == 0
    return !!valid;

}

仅仅提到with语句就会破坏整个包含函数——优化是以函数级别的粒度完成的,而不是每个语句。


很遗憾,使用标志位不是一个选项。我感觉V8的优化编译器有一天可能会将空的“with”块优化掉。如果你没有看到这种优化的可能性,为什么呢? - David Murdoch
@DavidMurdoch,没有进行任何优化,例如死代码删除,因为该函数通过提到“with”来禁用了优化。在这种优化发生之前,他们必须开始支持“with”在他们的优化编译器中,而我已经说过这将是疯狂的,因为哲学是支持合理的代码。 “with”是纯恶,并且在严格模式下甚至是语法错误,不是人们急于优化的合理代码。但如果您不想使用标志,则这是您最好的选择。 - Esailija
8年后:一位同事问我为什么调试器在技术上应该能看到某些函数中的变量,但实际上却看不到。我告诉他,我敢打赌是优化编译器将它们从作用域中移除了,因为它们没有被函数使用。我提到很久以前有人告诉我,在函数中使用 with({}){} 会导致优化器退出——至少在带有调试器的 Node v14.18.2 中仍然如此。 :-) - David Murdoch

4

要检查一个函数是否被特定的Node.js版本优化,您可以参考bluebird的Optimization Killers wiki。
我已经在Node 7.2上检查了3个解决方案:

  1. with({}) - 该函数已由TurboFan优化
  2. try {} catch(e) {} - 该函数已由TurboFan优化
  3. eval(''); - 该函数未被优化

因此,为确保禁用V8优化,您应该将eval('')添加到函数体中。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接