视频1 视频21 视频41 视频61 视频文章1 视频文章21 视频文章41 视频文章61 推荐1 推荐3 推荐5 推荐7 推荐9 推荐11 推荐13 推荐15 推荐17 推荐19 推荐21 推荐23 推荐25 推荐27 推荐29 推荐31 推荐33 推荐35 推荐37 推荐39 推荐41 推荐43 推荐45 推荐47 推荐49 关键词1 关键词101 关键词201 关键词301 关键词401 关键词501 关键词601 关键词701 关键词801 关键词901 关键词1001 关键词1101 关键词1201 关键词1301 关键词1401 关键词1501 关键词1601 关键词1701 关键词1801 关键词1901 视频扩展1 视频扩展6 视频扩展11 视频扩展16 文章1 文章201 文章401 文章601 文章801 文章1001 资讯1 资讯501 资讯1001 资讯1501 标签1 标签501 标签1001 关键词1 关键词501 关键词1001 关键词1501 专题2001
动态内存分配导致影响Javascript性能的问题
2020-11-27 22:02:24 责编:小采
文档


内存分配对性能的影响是很大的,分配内存本身需要时间,垃圾回收器回收内存也需要时间,所以应该尽量避免在堆里分配内存。不过直到最近优化HoLa cantk时,我才深刻的体会到内存分配对性能的影响,其中有一个关于arguments的问题挺有意思,写在这里和大家分享一下。

我要做的事情是用webgl实现canvas的2d API(这个话题本身也是挺有意思的,有空我们再讨论),drawImage是一个重要的函数,游戏会频繁的调用它,所以它的性能至关重要。drawImage的参数个数是可变的,它有三种形式:

  • drawImage(image, x, y)
  • drawImage(image, x, y, width, height)
  • drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh)
  • 第一个版本大概是这样实现的:

    function Context() {
    }
    Context.prototype.drawImage3 = function(image, x, y) {
     this.drawImage9(image, 0, 0, image.width, image.height, x, y, image.width, image.height);
    }
    Context.prototype.drawImage5 = function(image, dx, dy, dw, dh) {
     this.drawImage9(image, 0, 0, image.width, image.height, dx, dy, dw, dh);
    }
    Context.prototype.drawImage9 = function(image, sx, sy, sw, sh, dx, dy, dw, dh) {
     //DO IT
    }
    Context.prototype.drawImage = function(image, a, b, c, d, e, f, g, h) {
     var n = arguments.length;
     if(n === 3) {
     this.drawImage3(image, a, b);
     }else if(n === 5) {
     this.drawImage5(image, a, b, c, d);
     }else if(n === 9) {
     this.drawImage9(image, a, b, c, d, e, f, g, h);
     }
    }

    为了方便说明问题,我把测试程序出来:

    var image = {width:100, height:200};
    var ctx = new Context();
    function test() {
     var a = Math.random() * 100;
     var b = Math.random() * 100;
     var c = Math.random() * 100;
     var d = Math.random() * 100;
     var e = Math.random() * 100;
     var f = Math.random() * 100;
     var g = Math.random() * 100;
     var h = Math.random() * 100;
     for(var i = 0; i < 1000; i++) {
     ctx.drawImage(image, a, b);
     ctx.drawImage(image, a, b, c, d);
     ctx.drawImage(image, a, b, c, d, e, f, g, h);
     }
    }
    window.onload = function() {
     function loop() {
     test();
     requestAnimationFrame(loop);
     }
     requestAnimationFrame(loop);
    }

    用chrome的Profile查看CPU的使用情况时,我发现垃圾回收的时间比例很大,一般在4%以上。当时并没有怀疑到drawImage这个函数,理由很简单:

    这个函数很简单,它只是一个简单的分发函数,而drawImage9的实现相对来说要复杂得多。

    这里看不出有动态内存分配,也没有违背arguments的使用规则,只是使用了它的length属性。

    加trace_opt和trace_deopt参数运行时,drawImage被优化了,而且没有被反优化出来。

    Chrome的内存Profile只能看到没有被释放的对象,用它查看内存泄露比较容易。这里的问题并不是泄露,而是分配了然后又释放了,V8采用的分代垃圾回收器,这种短时存在的对象是由年轻代回收器管理器负责的,而年轻代回收器使用的半空间(semi-space)算法,这种大量短时间生存的对象,很快会耗尽其中一半空间,这时回收器需要把存活的对象拷贝到另外一半空间中,这就会耗费大量时间,而垃圾回收时会暂停JS代码执行,如果能避免动态内存分配,减少垃圾回收器的工作时间,就能提高程序的性能。

    没法在Chrome里查看动态分配内存的地方(呵呵,后面证实是我的无知),只好去硬着头皮看V8 JS引擎的代码,看看能不能找到频繁分配内存的地方,后来找到了V8统计内存分配的代码:

    void Heap::OnAllocationEvent(HeapObject* object, int size_in_bytes) {
     HeapProfiler* profiler = isolate_->heap_profiler();
     if (profiler->is_tracking_allocations()) {
     profiler->AllocationEvent(object->address(), size_in_bytes);
     }
     if (FLAG_verify_predictable) {
     ++allocations_count_;
     // Advance synthetic time by making a time request.
     MonotonicallyIncreasingTimeInMs();
     UpdateAllocationsHash(object);
     UpdateAllocationsHash(size_in_bytes);
     if (allocations_count_ % FLAG_dump_allocations_digest_at_alloc == 0) {
     PrintAlloctionsHash();
     }
     }
     if (FLAG_trace_allocation_stack_interval > 0) {
     if (!FLAG_verify_predictable) ++allocations_count_;
     if (allocations_count_ % FLAG_trace_allocation_stack_interval == 0) {
     isolate()->PrintStack(stdout, Isolate::kPrintStackConcise);
     }
     }
    }

    HeapProfiler已经有了内存分配的统计代码,Chrome里应该有对应的接口啊。再去看Chrome的Profile相关界面,最后发现需要在设置里勾选Record heap allocation stack traces,然后使用Record heap allocations功能,查看结果时选择Allocations,可以看到每个函数分配内存的次数。有时一个问题折腾你好久,解决之前百思不得其解,觉得难得不得了,而解决之后忍不住要苦笑,原来只是一层窗户纸!

    虽然还是不知道导致动态内存分配的原因(谁知道请告诉我),至少可以想法规避它:

    Context.prototype.drawImage = function() {
     var n = arguments.length;
     if(n === 3) {
     this.drawImage3.apply(this, arguments);
     }else if(n === 5) {
     this.drawImage5.apply(this, arguments);
     }else if(n === 9) {
     this.drawImage9.apply(this, arguments);
     }
    }

    总结

    下载本文
    显示全文
    专题