驯服V8架构的复杂性-CodeStubAssembler

本文将要介绍CodeStubAssembler(CSA),它是V8中的一个组件,对于在过去的多个V8版本中获得较大的性能提升来说,它是非常有用的工具。 CSA为V8团队提供在底层快速、可靠地优化JavaScript功能的能力,从而提高了团队的开发速度。

在V8中内建程序和手写汇编的简要历史

要了解CSA在V8中的作用,需要了解导致其发展的背景和历史。

V8在JavaScript代码之外使用多种技术进行性能压榨。 对于运行时间较长的JavaScript代码,V8的TurboFan优化编译器在加速ES2015 +全部功能规范,以达到最佳性能方面做得很好。 但是,V8还需要有效地执行短时运行的JavaScript代码,以获得良好的基准性能。 对于ECMAScript规范定义的所有JavaScript预定义对象上的所谓内置函数,尤其如此。

从历史上看,许多内置函数都是自托管的(self-hosted),也就是说,它们是由V8开发人员使用JavaScript编写的,尽管它们是V8内部特殊的实现。这些自托管内置程序的性能,依赖于V8优化用户JavaScript代码相同的机制。 与用户提供的代码一样,自托管的内置程序需要一个预热阶段,在该阶段,将收集类型反馈,并由优化编译器(TurboFan)进行编译。

尽管此技术在某些情况下可提供良好的内置性能,但我们仍可以做得更好。 规范中对Array.prototype上预定义函数的确切语义进行了详细的说明。 对于重要的和常见的特殊情况,V8的实现者可以通过理解规范来确切地了解这些内置函数应该如何工作,并且他们会利用这些知识来精心制作预先定制的、手动调整的版本。 这些经过优化的内建函数可以在不预热或无需调用优化编译器的情况下处理常见情况,因为通过构建的基线性能在首次调用前已经是最佳的。

为了从手写的内置JavaScript函数(以及其他一些快捷路径V8内置代码)中获得最佳性能,V8开发人员传统上使用汇编语言编写了优化的内置代码。 通过使用汇编,手写内置函数特别快速,尤其是避免了通过蹦床(trampolines)昂贵地调用V8的C ++代码,并利用了V8的基于寄存器的自定义ABI(Application binary interface,在内部使用它来调用JavaScript函数)。

由于手写汇编的优势,多年来,V8在每个平台上累积了成千上万行内置手写汇编代码。 所有这些手写内置函数都非常适合提高性能,但是新的语言功(注:应该指的是JavaScript语言标准)能始终处于进行标准化的调整状态,维护和扩展这些手写汇编既费力又容易出错。

进入重点—CodeStubAssembler

V8开发人员经历了多年的困境:是否可以创建具有手写汇编的优势而又不那么脆弱且难以维护的内置代码?

随着TurboFan的出现,这个问题的答案终于是“yes”。 TurboFan的后端使用跨平台的中间表示(IR)进行低级机器操作。 该低级机器IR被输入到指令选择器,寄存器分配器,指令调度器和代码生成器,它们在所有平台上产生非常好的代码。 后端还了解V8手写汇编内建程序中使用的许多技巧,例如 如何使用和调用基于寄存器的自定义ABI,如何支持机器级别的尾部调用(tail calls)以及如何在叶子函数中消除栈帧的构造。 这使TurboFan后端特别适合生成与V8的其余部分很好地集成的快速代码。

这种功能的组合使健壮的、可维护的手写汇编内置代码成为可能。 该团队构建了一个新的V8组件(称为CodeStubAssembler或CSA),该组件定义了一种在TurboFan后端构建的可移植汇编语言。 CSA添加了一个API,可以直接生成TurboFan机器级的IR,而无需编写和解析JavaScript,或应用TurboFan的JavaScript特定优化。 尽管这种用于内部加速V8引擎的代码快速生成方式只有V8开发人员才能使用,但是它以跨平台方式生成优化的汇编代码,让使用CSA构建内置程序的所有开发人员的JavaScirpt代码受益,包括对V8解释器Ignition性能至关重要的字节码处理程序。

 

                                                                              CSAJavaScript编译管道

CSA接口包括非常底层的操作,对于曾经编写汇编代码的任何人来说都是熟悉的。 例如,它包括诸如“从给定地址加载此对象指针”和“将这两个32位数相乘”之类的功能。 CSA在IR级别进行类型验证,以在编译时而不是运行时捕获错误。 例如,它可以确保V8开发人员不会意外地使用从内存加载的对象指针作为32位乘法的输入,手写汇编的存根不可能实现这种类型的验证。

一个CSA例子

为了更好地了解CSA的功能,让我们来看一个简单的例子。 我们将在V8中添加一个新的内部内置函数,该函数将返回对象(如果它是字符串)的字符串长度。 如果输入对象不是字符串,则内置函数将返回undefined。

首先,我们在V8的buildin-definitions.h文件中的BUILTIN_LIST_BASE宏中添加一行,该行声明一个新的名为GetStringLength的内置函数,并指定它具有一个用常数kInputObject标识的单个输入参数:

  TFS(GetStringLength, kInputObject)

TFS宏指用标准的CodeStub链接将内置代码声明为TurboFan内置,简单来说就是使用CSA生成其代码,并且参数通过寄存器传递。

接下来我们在builtins-string-gen.cc文件中定义内置内容:

TF_BUILTIN(GetStringLength, CodeStubAssembler) {
  Label not_string(this);

  // Fetch the incoming object using the constant we defined for
  // the first parameter.
  Node* const maybe_string = Parameter(Descriptor::kInputObject);

  // Check to see if input is a Smi (a special representation
  // of small numbers). This needs to be done before the IsString
  // check below, since IsString assumes its argument is an
  // object pointer and not a Smi. If the argument is indeed a
  // Smi, jump to the label |not_string|.
  GotoIf(TaggedIsSmi(maybe_string), &not_string);

  // Check to see if the input object is a string. If not, jump to
  // the label |not_string|.
  GotoIfNot(IsString(maybe_string), &not_string);

  // Load the length of the string (having ended up in this code
  // path because we verified it was string above) and return it
  // using a CSA "macro" LoadStringLength.
  Return(LoadStringLength(maybe_string));

  // Define the location of label that is the target of the failed
  // IsString check above.
  BIND(&not_string);

  // Input object isn't a string. Return the JavaScript undefined
  // constant.
  Return(UndefinedConstant());
}

请注意,在上面的示例中,使用了两种类型的指令。 一些原始的CSA指令可以直接转换为一两条汇编指令,例如GotoIf和Return。 有一组固定的预定义CSA原始指令,这些指令与V8所支持的每一种芯片架构上常用的汇编指令大致相对应。 该示例中的其他指令是宏指令,例如LoadStringLength,TaggedIsSmi和IsString,它们是一些便捷函数,用于输出一个或多个原始指令或内联宏指令。 宏指令用于封装常用的V8惯用语法实现,以便重用。 它们可以任意长,V8开发人员可以在需要时轻松定义新的宏指令。

做完上述更改后,重新编译V8,然后我们运行mksnapshot,这个工具编译内置代码并将其放入V8的snapshot。运行mksnapshot时,我们带上—print-code命令行参数,这个参数会打印所有内置代码的汇编代码。如果我们用grep在输出内容中捕获GetStringLength,我们可以在X64平台得到下面的结果(输出的代码做了一些整理,以方便阅读)。

  test al,0x1

  jz not_string

  movq rbx,[rax-0x1]

  cmpb [rbx+0xb],0x80

  jnc not_string

  movq rax,[rax+0xf]

  retl

not_string:

  movq rax,[r13-0x60]

  retl

在32位ARM平台上,mksnapshot生成以下代码:

  tst r0, #1

  beq +28 -> not_string

  ldr r1, [r0, #-1]

  ldrb r1, [r1, #+7]

  cmp r1, #128

  bge +12 -> not_string

  ldr r0, [r0, #+7]

  bx lr

not_string:

  ldr r0, [r10, #+16]

  bx lr

即使新的内置程序使用非标准(至少是非C ++)的调用约定,也可以为其编写测试用例。 可以将以下代码添加到test-run-stubs.cc文件中,以在所有平台上测试内置程序:

TEST(GetStringLength) {

  HandleAndZoneScope scope;

  Isolate* isolate = scope.main_isolate();

  Heap* heap = isolate->heap();

  Zone* zone = scope.main_zone();


  // Test the case where input is a string

  StubTester tester(isolate, zone, Builtins::kGetStringLength);

  Handle<String> input_string(

      isolate->factory()->

        NewStringFromAsciiChecked("Oktoberfest"));

  Handle<Object> result1 = tester.Call(input_string);

  CHECK_EQ(11, Handle<Smi>::cast(result1)->value());


  // Test the case where input is not a string (e.g. undefined)

  Handle<Object> result2 =

      tester.Call(factory->undefined_value());

  CHECK(result2->IsUndefined(isolate));

}

有关将CSA用于不同种类的内建程序的更多详细信息及示例,请参阅这个页面,中文翻译参阅这里。。

一个V8开发效率倍增器

CSA不仅仅是针对多个平台的通用汇编语言,和我们以前为每种体系架构的手写代码相比,它在实现新功能时大幅缩短了开发时间。 它可以提供手写汇编的所有好处,同时保护开发人员避免危险陷阱的影响:

  • 借助CSA,开发人员可以使用跨平台的低级基元集合来编写内置代码,这些基元可以直接转换为汇编指令。 CSA的指令选择器确保该代码在V8面向的所有平台上都是最优的,而无需V8开发人员成为每种平台上汇编语言的专家。
  • CSA的接口具有可选的类型,以确保由低级别生成的汇编操做的值是代码作者期望的类型。
  • 汇编指令中的寄存器分配由CSA自动完成的,而不是由人工手动完成:这包括构建栈帧(栈帧定义见附录);以及如果内置代码使用的寄存器多于可用寄存器或进行调用时,将值溢出到栈里。 这消除了困扰手写汇编内置程序的一整套细微、难以发现的错误。通过让生成的代码降低脆弱性,CSA大大减少了编写正确底层内置程序所需的时间。
  • CSA了解ABI调用约定(标准C ++的和基于内部V8寄存器的),从而可以轻松地在CSA生成的代码与V8的其他部分之间进行互操作。
  • 由于CSA代码是C ++的,因此很容易将常见的代码生成模式封装在宏里,从而可以轻松地在许多内置函数中重复使用。
  • 由于V8使用CSA生成用于Ignition的字节码处理程序,因此很容易将基于CSA的内置函数直接内嵌到字节码处理程序中以提高解释器的性能。
  •  V8的测试框架支持从C ++测试CSA功能和CSA生成的内置程序,而无需编写汇编适配器。

总而言之,CSA已经改变了V8开发的游戏规则,它大大提高了团队优化V8的能力。 这意味着我们能够为V8的嵌入程序更快地优化更多JavaScript语言。

 

附录

栈帧

什么是栈帧(Stack Frame)呢?
每一次函数的调用,都会在调用栈(call stack)上维护一个独立的栈帧(stack frame).每个独立的栈帧一般包括:

  • 函数的返回地址和参数
  • 临时变量: 包括函数的非静态局部变量以及编译器自动生成的其他临时变量
  • 函数调用的上下文
    栈是从高地址向低地址延伸,一个函数的栈帧用ebp 和 esp 这两个寄存器来划定范围.ebp 指向当前的栈帧的底部,esp 始终指向栈帧的顶部;
    ebp 寄存器又被称为帧指针(Frame Pointer);
    esp 寄存器又被称为栈指针(Stack Pointer);

 


 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值