多态内联缓存如何与可变类型一起工作?

发布于 2024-07-26 09:49:28 字数 300 浏览 2 评论 0原文

多态内联缓存(PIC)的工作原理是根据对象的类型缓存实际方法,以避免昂贵的查找过程(通常是哈希表查找)。

如果类型对象是可变的(即该方法可能在运行时被猴子修补成不同的东西),那么如何处理类型比较?

我提出的一个想法是一个“类计数器”,每次调整一个方法时它都会增加,但是这在一个经过大量猴子修补的环境中似乎会非常昂贵,因为它会杀死所有的 PIC那个类,即使它们的方法没有改变。

我确信一定有一个好的解决方案,因为这个问题直接适用于 JavaScript,并且 AFAIK 所有三个大型 JavaScript 虚拟机都有 PIC。

A polymorphic inline cache(PIC) works by caching the actual method by the type of the object, in order to avoid the expensive lookup procedures (usually a hashtable lookup).

How does one handle the type comparison if the type objects are mutable (i.e. the method might be monkey patched into something different at run time)?

The one idea I've come up with would be a "class counter" that gets incremented each time a method is adjusted, however this seems like it would be exceptionally expensive in a heavily monkey patched environment, since it would kill all the PICs for that class, even if the methods for them weren't altered.

I'm sure there must be a good solution to this, as this issue is directly applicable to JavaScript, and AFAIK all three of the big JavaScript virtual machines have PICs.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

没有伤那来痛 2024-08-02 09:49:28

在V8中,我假设monkeypatching会改变对象的“隐藏类”(“map”是SELF术语)。 这对于你修补对象本身的猴子来说是有效的。

如果你猴子修补这个类(你能用 JS 来做吗?),我猜它会使所有 PIC 失效,因为这可能很少见。 或者,它可能会重新编译旧方法以直接分派到新方法(我猜是在类型检查之后)

顺便说一句,我认为其他“三大”实际上并不使用 PIC。 我猜你指的是松鼠鱼和踪迹猴。 前者是口译员,后者专注于追踪方法,我不记得听说过任何有关 PIC 的事情。 事实上,我不认为tracemonkey 对对象有任何酷的作用,但我可能是错的。

In V8, I would assume that monkeypatching would change the "hidden class" ("map" is SELF terminology) of the object. That would work in you monkey patched the object itself.

If you monkey patched the class (can you do that is JS?), I would guess it invalidates all PICs, since this is probably rare. Alternatively, it might recompile the old method to dispatch straight to the new method (after a type check I guess)

As an aside, I don't think the other "big 3" use PICs, actually. I presume you mean squirrelfish and tracemonkey. The former is an interpreter, and the latter focusses on the tracing approach, and I don't recall hearing anything about PICs. In fact, I don't think tracemonkey does anything cool at all for objects, but I could be wrong.

神仙妹妹 2024-08-02 09:49:28

使用类型推断:

SpiderMonkey主要使用类型推断来确定正在访问哪些属性; 如果类型推断无法找到正在访问的对象的确切形状,SpiderMonkey 使用 PIC(多态内联缓存)来存储查找结果。

分析反馈的另一种形式是基线 JIT 的多态内联缓存。 多态内联缓存是一种优化动态调度的经典技术,起源于 Smalltalk 社区。

反优化是类型推断失败时发生的过程的名称:

当然,JavaScript 是一种动态类型语言,有时对对象隐藏类的假设会不正确,在这种情况下,V8 将“去优化”并恢复到方法调用的原始版本检查对象隐藏类。

为此,需要多个编译器:

从技术上讲,这意味着编译器实际上是两个编译器:一个基本编译器和一个“优化器”。 (或者更多,如果我们谈论的是 JSC 和 SpiderMonkey)。 这个概念非常合理,可以产生令人难以置信的性能,但有一个细微差别:优化后的代码可能在各个地方“去优化”,而不仅仅是在入口点,这意味着环境(局部变量、参数、上下文)应该是绘制地图并四处移动。

Ion 作为优化编译器,不支持在调试模式检查下进行编译。 事实上,实现这种支持的效用是值得怀疑的,因为它的优化最多只会使 onStep 调用变得不恰当。 为了支持调试模式,堆栈上的优化代码将被取消优化并转出到基线。 也就是说,堆栈上的 Ion 帧将被与当前正在执行 Ion 代码的同一位置相对应的重构基线帧覆盖。

调试是通过动态去优化完成的:

我们可以做得更好。 90 年代的梦想,体现在 Self 语言中,在 JavaScript 中得以实现。 我们可以通过栈上替换技术来调整 Self 的动态去优化,以调试优化的代码。 调试模式 OSR 的核心思想很简单:当需要调试时,我们在堆栈上取消优化并重新编译 JIT 代码,同时修补返回地址。 人们可能会说它几乎是优雅的,如果不是因为它对堆栈帧造成的严格暴力。

考虑以下 JavaScript 函数:

function foo(o) { return o.f + o.g; }

在此示例中,属性访问可能会导致从堆中已知位置的简单加载到 getter 调用甚至复杂的 DOM 陷阱,例如 o 是一个文档对象和 f 是页面中元素的名称。 基线 JIT 最初将作为完全多态调度执行这些属性访问。 但这样做时,它将记录所采取的步骤,然后将堆访问就地修改为必要步骤的缓存,以便将来重复类似的访问。 例如,如果对象在距对象基点偏移 16 处具有属性 f,则代码将被修改为首先快速检查传入对象是否包含属性 f< /code> 在偏移量 16 处,然后执行加载。 这些缓存被称为内联缓存,因为它们完全表示为生成的机器代码。 之所以说它们是多态的,是因为如果遇到不同的对象结构,则会修改机器代码以在进行完全动态属性查找之前切换先前遇到的对象类型。 当 DFG 编译此代码时,它将检查内联缓存是否是单态的(仅针对一个对象结构进行优化),如果是,它将仅对该对象结构进行检查,然后直接加载。 在此示例中,如果 o 始终是一个在不变偏移处具有属性 fg 的对象,则 DFG 只需要对 o< 发出一次类型检查/code> 随后是两次直接加载。


数据流即时编译器 (DFG JIT) 优化管道

Faster-Than-Light 即时编译器 (FTL JIT) 优化管道

参考

Type inference is used:

SpiderMonkey mainly uses type inference to determine which properties are being accessed; in cases where type inference does not find the exact shape of the object being accessed, SpiderMonkey uses a PIC (polymorphic inline caches) to store the result of the lookup.

An additional form of profiling feedback are the Baseline JIT's polymorphic inline caches. Polymorphic inline caches are a classic technique for optimizing dynamic dispatch that originated in the Smalltalk community.

De-optimization is the name of the process that happens when type inference fails:

Of course, JavaScript being a dynamically typed language, ocassionally the assumption about the hidden class of the object will be incorrect, and in that case V8 will "de-optimize" and revert back to the original version of the method call in which the objects hidden class is checked.

Multiple compilers are necessary for this to work:

Technically it means that the compiler is in fact two compilers: a base compiler and an "optimizer". (Or even more, if we are talking about JSC and SpiderMonkey). The concept is quite sound and can yield incredible performance, but there is a nuance: the optimized code may be "deoptimized" in various places, not just at the entry point, meaning that the environment (local variables, arguments, context) should be mapped and moved around.

Ion, being the optimizing compiler, does not support compiling in debug mode checks. Indeed, implementing such support is of dubious utility, as its optimizations would make onStep invocations at best infelicitous. To support debug mode, optimized code on the stack are deoptimized and bailed out to Baseline. That is, the Ion frame on the stack is overwritten with a reconstructed Baseline frame corresponding to the same location in which Ion code is currently executing.

Debugging is done via dynamic deoptimization:

We can do better. The dream of the 90s, embodied in the Self language, is alive in JavaScript. We can adapt Self’s dynamic deoptimization via on-stack replacement technique to debug even optimized code. The core idea of a debug mode OSR is simple: when debugging is required, we deoptimize and recompile JITed code on the stack, patching return addresses as we go. One might say it is almost elegant, if not for the exacting violence it inflicts on stack frames.

Consider the following JavaScript function:

function foo(o) { return o.f + o.g; }

In this example, the property accesses may lead to anything from a simple load from well-known locations in the heap to invocations of getters or even complicated DOM traps, like if o were a document object and f were the name of an element in the page. The Baseline JIT will initially execute these property accesses as fully polymorphic dispatch. But as it does so it will record the steps it takes, and will then modify the heap accesses in-place to be caches of the necessary steps to repeat a similar access in the future. For example, if the object has a property f at offset 16 from the base of the object, then the code will be modified to first quickly check if the incoming object consists of a property f at offset 16 and then perform the load. These caches are said to be inline because they are represented entirely as generated machine code. They are said to be polymorphic because if different object structures are encountered, the machine code is modified to switch on the previously encountered object types before doing a fully dynamic property lookup. When the DFG compiles this code, it will check if the inline cache is monomorphic – optimized for just one object structure – and if so, it will emit just a check for that object structure followed by the direct load. In this example, if o was always an object with properties f and g at invariant offsets, then the DFG would only need to emit one type check for o followed by two direct loads.

Data Flow Just-in-Time Compiler (DFG JIT) Optimization Pipeline

Faster-Than-Light Just-in-Time Compiler (FTL JIT) Optimization Pipeline

References

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文