【问题标题】:JavaScript: Direct code vs CPS styled generated code performance comparisonJavaScript:直接代码与 CPS 样式生成的代码性能比较
【发布时间】:2012-08-24 22:24:50
【问题描述】:

在我的应用程序中,我正在生成遵循 CPS 样式的 JavaScript 代码。我“不”使用任何“延续”。没有异步行为,没有暂停和恢复,也没有回调。

只是代码遵循continuation passing style 的编程。

功能有许多阶段,每个阶段都进行处理并将结果传递给其延续。

我发现 CPS 样式代码的性能很差。以直接样式编写的代码几乎比 CPS 样式代码快 150 倍。

请检查以下代码。
下面两个代码都等价于

var res = data.store.bookshelf.book.author;

直接样式代码:

var data = { store : { bookshelf : {book : {author:"Douglas Crockford"}}}};
var t1 = new Date().getTime();
for(var i = 0; i < 1000*1000*100; i+=1){      
  var temp0 = data;
  var temp1 = temp0.store;
  var temp2 = temp1.bookshelf;
  var temp3 = temp2.book;
  var temp4 = temp3.author;
  var res = temp4;
}
var t2 = new Date().getTime();
console.log(t2-t1);

上面的代码运行了将近 95 毫秒。

CPS 样式代码:

var data = { store : { bookshelf : {book : {author:"Douglas Crockford"}}}};

// return the variable to the continuation
function cps_VARREF(x,f){
  return f(x);
}
// get the value of the property from the variable and pass it to the continuation
function cps_CHILD(x,child,f){
  return f(x[child]);
}
// simply return the input value, essentially closing the continuation chain
function ret_(x){
  return x;
}

var t1 = new Date().getTime();
for(var i = 0; i < 1000*1000*100; i+=1){
 var res = function(c_){
    return cps_VARREF(data,function(x1){
    return cps_CHILD(x1,"store",function(x2){
    return cps_CHILD(x2,"bookshelf",function(x3){
    return cps_CHILD(x3,"book",function(x4){
    return cps_CHILD(x4,"author",c_);});});});});}(ret_);
}
var t2 = new Date().getTime();
console.log(t2-t1);

以上 CPS 风格的代码运行时间为 15000 毫秒

我可以做些什么来改进 CPS 样式的代码?还是 JavaScript 天生不适合 CPS 样式的代码?

以上测试是在node.js 0.6.12版本上完成的

有人可以解释一下这个问题吗?

谢谢,

【问题讨论】:

标签: javascript node.js continuation-passing


【解决方案1】:

至少有两个可能导致急剧放缓的原因。首先是您将“本机”属性查找替换为动态查找。

V8 尽可能优化对象,因此访问属性更快。它不是使用哈希表按名称查找属性,而是跟踪内部“类”,以便它可以从已知地址查找属性。所以data.store 只是一个快速的指针比较,以确保对象是预期的类型并加载索引指针。

但在cps_CHILD 函数中,它无法进行优化,因为它不知道要提前访问什么属性(并且每次调用它都会改变)。动态查找强制 V8 回退到哈希表查找,这比优化的静态查找要慢。

另一个问题是函数调用的开销。每次将嵌套函数传递给下一个函数时,都必须重新创建每个嵌套函数。它们不应该每次都编译,但仍然需要在新的上下文中创建。

【讨论】:

  • 谢谢@Matthew。关于属性的“本机”与动态查找,我之前曾在 SO 中提出过问题,我得到的答案是两者都是相同的。我的测试结果还表明,两者的性能相同。 stackoverflow.com/questions/11580448/…
  • @weima 如果您总是使用字符串字面量查找相同的属性,那是真的。编译器足够聪明,可以同样对待它。问题是当您将属性名称传递给函数时,该函数不会提前知道字符串是什么,因此无法对其进行优化。
  • 我创建了一个测试用例来展示差异。使用常量字符串文字进行索引几乎与 obj.property 语法相同,但两者的速度都是每次使用不同属性名称的动态查找的两倍多。测试用例:jsperf.com/static-vs-dynamic
  • 在 V8(和其他现代 JS 引擎)中,您通常可以通过将 JavaScript 视为静态语言来获得最佳性能。当然,您通常不应该牺牲可读性或利用语言的动态特性来仅仅挤出几微秒的性能。但是对于您已确定(通过分析)会导致实际性能问题的代码,在完成更高级别的优化后需要牢记这一点。
  • 非常感谢您的回复。我决定不生成 CPS 样式代码,而是尝试生成普通样式代码。谢谢:)
【解决方案2】:

你应该知道有些东西是在运行时解析的,比如你在循环中输入的那些匿名函数。在每个新的“i”上,将再次创建新的“构造函数”及其每个匿名函数的原型。这就是为什么它更慢。这是新的测试。尝试为每个匿名函数定义实际函数,将它们嵌套在循环之外,它应该可以提高程序的性能。

这里是代码,它和 CPS 风格的代码一样深入,但只有一次函数被定义

var data = { store : { bookshelf : {book : {author:"Douglas Crockford"}}}};

function getValueFrom(data){
    return data;
}

function getAuthorFrom(data){
    return getValueFrom(data);
}

function getBookFrom(data){
    return getAuthorFrom(data["book"]);
}

function getBookShelfFrom(data){
    return getBookFrom(data["bookshelf"]);
}

function getStoreFrom(data){
    return getBookShelfFrom(data["store"]);
}

function getAuthor(data){
    return getAuthor(data);
}

var t1 = new Date().getTime();
for(var i = 0; i < 1000*1000*100; i+=1){
 var res = getStoreFrom(data);
}
var t2 = new Date().getTime();
console.log(t2-t1);

它的运行速度提高了 4-5 倍。由于 JS 引擎需要寻找函数原型,因此速度仍然较慢,因此可以将其放在堆上执行。在第一种情况下(非 CSP 样式),当您使用点访问属性 JS 引擎时,仅通过键(JS 对象属性)查询哈希以获取其值。它的工作速度要快得多,因为它只需要处理内存引用。

【讨论】:

  • 这不是延续传递风格(带有回调),虽然我认为你的解释很正确。
  • 这是上面代码的重点,它应该解释当你使用 CPS 时是什么降低了性能
猜你喜欢
  • 1970-01-01
  • 2013-05-22
  • 2010-09-28
  • 2016-04-27
  • 2020-01-23
  • 2012-09-02
  • 2014-03-21
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多