The story of a V8 performance cliff in React

Bài tóm tắt V8 (Google) 2019 3. Quy mô lớn Độ khó: Nâng cao

V8 (Google) · 2019 · 3. Quy mô lớn · Độ khó: Nâng cao — bài gốc

Bối cảnh

  • V8 không lưu số JavaScript (Float64 theo spec) theo một cách cố định: số nguyên 31 bit có dấu là Smi, số khác là HeapNumber. Mỗi trường của object được đánh dấu "representation" trên shape (hidden class): Smi, Double (giá trị nằm trong MutableHeapNumber cập nhật tại chỗ), HeapObject, Tagged.
  • Khi một trường đổi representation (ví dụ từ Smi sang Double), V8 tạo nhánh shape mới, đánh dấu nhánh cũ là deprecated và dời các object sang dần (lazy, ở lần truy cập sau) thay vì quét bộ nhớ.
  • Đội React, khi profile một ứng dụng thật, gặp một "vách hiệu năng" trong lõi React (issue #14365).

Thay đổi chính

  • Nguyên nhân: mỗi FiberNode có các trường thời gian cho profiling, khởi tạo 0 hoặc -1 (Smi), sau đó nhận timestamp thực từ performance.now() (phải thành Double); constructor còn gọi Object.preventExtensions(this). Khi đổi representation, V8 tìm "split shape" để phát lại chuỗi transition, nhưng split shape còn extensible trong khi shape hiện tại thì không; V8 không biết phát lại nên bỏ cuộc, tạo một shape mồ côi riêng cho từng object. Ứng dụng React có hàng chục nghìn FiberNode → hàng chục nghìn shape rời rạc, inline cache vô dụng.
  • Sửa ở cả hai phía: V8 7.4 phát lại đúng transition qua bước non-extensible, nên các node cùng trỏ về shape mới và nhánh deprecated được GC dọn. React tự giảm nhẹ bằng cách gán Number.NaN cho các trường thời gian trong constructor để chúng mang representation Double ngay từ đầu, rồi mới gán giá trị thật.
  • V8 nghi rằng cơ chế deprecate/migrate shape gây hại nhiều hơn lợi trên web thật (hiệu năng, bộ nhớ, độ phức tạp), nhất là khi pointer compression ra đời, và đang tìm cách làm việc đổi representation rẻ hơn.

Vì sao

  • Theo V8, lỗi này đặc thù cho V8 và developer nói chung không nên tối ưu theo một phiên bản engine; nhưng không trộn kiểu dữ liệu trong một trường (ví dụ không khởi tạo trường số bằng null) giúp mọi engine.

Bài học cho SWE

  • Một tối ưu ngầm của runtime có thể có vách: hành vi đúng nhưng chậm hàng bậc khi gặp một tổ hợp hiếm (đổi representation + non-extensible). Chỉ profile trên ứng dụng thật mới lộ ra.
  • Sửa lỗi hiệu năng xuyên tầng thường cần cả hai phía: bên nền tảng sửa gốc, bên dùng giảm nhẹ để không phải chờ phiên bản mới tới tay người dùng.

Liên quan