Hotspot什么时候可以在栈上分配对象?
When can Hotspot allocate objects on the stack?
从 Java 6 左右开始,Hotspot JVM 可以进行逃逸分析并在堆栈上而不是在垃圾收集堆上分配非逃逸对象。这会加快生成代码的速度并减少垃圾收集器的压力。
Hotspot可以堆栈分配对象的规则是什么?换句话说,我什么时候可以依靠它来进行堆栈分配?
编辑:这个问题是重复的,但是(IMO)下面的答案比原始问题的答案更好。
我做了一些实验以查看 Hotspot 何时能够进行堆栈分配。事实证明,它的堆栈分配比您基于 available documentation 所预期的要有限得多。 Choi "Escape Analysis for Java" 所引用的论文表明,仅分配给局部变量的对象始终可以进行堆栈分配。但事实并非如此。
所有这些都是当前 Hotspot 实现的实现细节,因此它们可能会在未来的版本中发生变化。这是指我的 OpenJDK 安装版本 1.8.0_121 for X86-64.
基于大量实验的简短总结似乎是:
如果
,Hotspot 可以堆栈分配一个对象实例
- 它的所有用法都是内联的
- 它从不分配给任何静态或对象字段,只分配给局部变量
- 在程序的每一点,哪些局部变量包含对对象的引用必须是 JIT 时间可确定的,并且不依赖于任何不可预测的条件控制流。
- 如果对象是一个数组,它的大小在 JIT 时间必须是已知的并且索引到它必须使用 JIT 时间常量。
要了解这些条件何时成立,您需要对 Hotspot 的工作原理有相当多的了解。依赖 Hotspot 在特定情况下确定地进行堆栈分配可能会有风险,因为涉及到很多非本地因素。尤其是要知道是否所有内容都是内联的可能很难预测。
实际上,简单的迭代器通常是堆栈可分配的,如果你只是用它们来迭代的话。对于复合对象,只有外部对象可以进行堆栈分配,因此列表和其他集合总是会导致堆分配。
如果您有 HashMap<Integer,Something>
并在 myHashMap.get(42)
中使用它,42
可能会在测试程序中进行堆栈分配,但不会在完整的应用程序中使用,因为您可以确保整个程序中HashMaps中的key对象会多于两种类型,因此key上的hashCode和equals方法不会内联。
除此之外,我没有看到任何普遍适用的规则,这将取决于代码的具体情况。
热点内部结构
首先要知道的是逃逸分析是在内联之后执行的。这意味着 Hotspot 的逃逸分析在这方面比 Choi 论文中的描述更强大,因为从方法返回但对调用方方法而言是本地的对象仍然可以进行堆栈分配。因此,如果您这样做,迭代器几乎总是可以分配堆栈。 for(Foo item : myList) {...}
(myList.iterator()
的实现非常简单,通常如此。)
Hotspot 仅在确定方法是 'hot' 后才编译方法的优化版本,因此很多时候不是 运行 的代码根本不会得到优化,在这种情况下有没有任何堆栈分配或内联。但是对于那些你通常不关心的方法。
内联
内联决策基于 Hotspot 首先收集的分析数据。声明的类型并不重要,即使一个方法是虚拟的,Hotspot 也可以根据它在分析过程中看到的对象类型将其内联。分支(即 if 语句和其他控制流结构)也有类似的情况:如果在分析期间 Hotspot 从未看到某个分支被采用,它将基于该分支从未被采用的假设来编译和优化代码。在这两种情况下,如果 Hotspot 无法证明其假设始终为真,它将在编译后的代码中插入检查,称为 'uncommon traps',如果命中此类陷阱,Hotspot 将取消优化并可能重新优化采用新信息考虑在内。
Hotspot 将描述哪些对象类型作为接收者出现在哪些调用站点。如果 Hotspot 在调用点只看到一个类型或两个不同的类型,它能够内联被调用的方法。如果只有一两个非常常见的类型,而其他类型出现的频率要低得多,Hotspot 也应该仍然能够内联常见类型的方法,包括检查需要获取哪些代码。 (虽然我不完全确定最后一个案例有一个或两个常见类型和更不常见的类型)。如果有两种以上的常见类型,Hotspot 根本不会内联调用,而是生成机器码用于间接调用。
'Type' 这里指的是对象的确切类型。不考虑已实现的接口或共享的 super类。即使在一个调用点出现不同的接收者类型,但它们都继承了相同的方法实现(例如,多个 类 都从 Object
继承了 hashCode
),Hotspot 仍然会生成一个间接的打电话而不是内联。 (所以i.m.o。热点在这种情况下是相当愚蠢的。我希望未来的版本改进这一点。)
Hotspot 也只会内联不太大的方法。 'Not too big' 由 -XX:MaxInlineSize=n
和 -XX:FreqInlineSize=n
选项决定。 JVM 字节码大小低于 MaxInlineSize 的可内联方法始终被内联,如果调用是 'hot',则 JVM 字节码大小低于 FreqInlineSize 的方法被内联。较大的方法从不内联。默认情况下,MaxInlineSize 是 35,FreqInlineSize 是平台相关的,但对我来说是 325。所以如果你想要内联它们,请确保你的方法不会太大。有时它可以帮助从一个大方法中分离出公共路径,以便它可以内联到它的调用者中。
分析
关于分析需要了解的一件重要事情是,分析站点是基于 JVM 字节码的,它本身没有以任何方式内联。所以如果你有例如静态方法
static <T,U> List<U> map(List<T> list, Function<T,U> func) {
List<U> result = new ArrayList();
for(T item : list) { result.add(func.call(item)); }
return result;
}
将 SAM Function
可调用映射到列表和 returns 转换后的列表,Hotspot 会将对 func.call
的调用视为单个程序范围的调用站点。您可以在程序的多个位置调用此 map
函数,在每个调用点传递不同的函数(但一个调用点传递相同的函数)。在那种情况下,您可能希望 Hotspot 能够内联 map
,然后还可以调用 func.call
,因为每次使用 map
时,只有一个 func
类型.如果是这样,Hotspot 将能够非常紧密地优化循环。不幸的是,Hotspot 不够智能。它只为 func.call
调用站点保留一个配置文件,将您传递给 map
的所有 func
类型集中在一起。您可能会使用 func
的两个以上不同实现,因此 Hotspot 将无法内联对 func.call
的调用。 Link for more details, and archived link 因为原件似乎不见了。
(顺便说一句,在 Kotlin 中,等效循环可以完全内联,因为 Kotlin 编译器可以在字节码级别内联调用。因此对于某些用途,它可能比 Java.)
标量替换
另一件需要知道的重要事情是 Hotspot 实际上并没有实现 objects 的堆栈分配。相反,它实现了 标量替换 ,这意味着一个对象被解构为其组成字段,并且这些字段像普通局部变量一样分配堆栈。这意味着根本没有留下任何对象。标量替换仅在永远不需要创建指向堆栈分配对象的指针时才有效。某些形式的堆栈分配,例如C++ 或 Go 能够在堆栈上分配完整的对象,然后将指向它们的引用或指针传递给被调用的函数,但在 Hotspot 中这是行不通的。因此,如果需要将对象引用传递给非内联方法,即使引用不会逃逸被调用的方法,Hotspot 也会始终堆分配此类对象。
原则上 Hotspot 在这方面可以更聪明,但现在不是。
测试程序
我使用以下程序和变体来查看 Hotspot 何时进行标量替换。
// Minimal example for which the JVM does not scalarize the allocation. If field is final, or the second allocation is unconditional, it will.
class Scalarization {
int field = 0xbd;
long foo(long i) { return i * field; }
public static void main(String[] args) {
long result = 0;
for(long i=0; i<100; i++) {
result += test();
}
System.out.println("Result: "+result);
}
static long test() {
long ctr = 0x5;
for(long i=0; i<0x10000; i++) {
Scalarization s = new Scalarization();
ctr = s.foo(ctr);
if(i == 0) s = new Scalarization();
ctr = s.foo(ctr);
}
return ctr;
}
}
如果您使用 javac Scalarization.java; java -verbose:gc Scalarization
编译并 运行 这个程序,您可以看到标量替换是否根据垃圾收集次数起作用。如果标量替换有效,则我的系统上没有发生垃圾回收,如果标量替换无效,我会看到一些垃圾回收。
Hotspot 能够缩放的变体 运行 比它不能缩放的版本快得多。我验证了生成的机器代码 (instructions) 以确保 Hotspot 没有进行任何意外的优化。如果热点能够标量替换分配,那么它还可以对循环进行一些额外的优化,将其展开几次迭代,然后将这些迭代组合在一起。因此,在标量化版本中,有效循环计数较低,每次迭代都会执行多个源代码级迭代的工作。所以速度差异不仅仅是由于分配和垃圾收集开销。
观察结果
我尝试了上述程序的多种变体。标量替换的一个条件是对象绝不能分配给对象(或静态)字段,并且大概也不能分配给数组。所以在代码中
Foo f = new Foo();
bar.field = f;
Foo
对象不能被标量替换。即使 bar
本身被标量替换,并且如果您不再使用 bar.field
,这仍然成立。所以一个对象只能分配给局部变量。
仅此还不够,Hotspot 还必须能够在 JIT 时静态地确定哪个对象实例将成为调用的目标。例如,使用 foo
和 test
的以下实现并删除 field
会导致堆分配:
long foo(long i) { return i * 0xbb; }
static long test() {
long ctr = 0x5;
for(long i=0; i<0x10000; i++) {
Scalarization s = new Scalarization();
ctr = s.foo(ctr);
if(i == 50) s = new Scalarization();
ctr = s.foo(ctr);
}
return ctr;
}
而如果您随后删除第二次分配的条件,则不会再发生堆分配:
static long test() {
long ctr = 0x5;
for(long i=0; i<0x10000; i++) {
Scalarization s = new Scalarization();
ctr = s.foo(ctr);
s = new Scalarization();
ctr = s.foo(ctr);
}
return ctr;
}
在这种情况下,Hotspot 可以静态确定哪个实例是每次调用 s.foo
.
的目标
另一方面,即使对 s
的第二个赋值是 Scalarization
的子类且实现完全不同,只要赋值是无条件的,Hotspot 仍会标量分配。
Hotspot 似乎无法将对象移动到先前标量替换的堆中(至少在没有取消优化的情况下)。标量替换是一个孤注一掷的事情。所以在最初的 test
方法中,Scalarization
的两次分配总是发生在堆上。
条件
一个重要的细节是 Hotspot 将根据其分析数据预测条件。如果从未执行条件赋值,Hotspot 将在该假设下编译代码,然后可能能够进行标量替换。如果在稍后某个时间点确实采用了条件,Hotspot 将需要使用这个新假设重新编译代码。新代码不会进行标量替换,因为 Hotspot 无法再静态确定后续调用的接收者实例。
例如在 test
的这种变体中:
static long limit = 0;
static long test() {
long ctr = 0x5;
long i = limit;
limit += 0x10000;
for(; i<limit; i++) { // In this form if scalarization happens is nondeterministic: if the condition is hit before profiling starts scalarization happens, else not.
Scalarization s = new Scalarization();
ctr = s.foo(ctr);
if(i == 0xf9a0) s = new Scalarization();
ctr = s.foo(ctr);
}
return ctr;
}
条件赋值在程序的生命周期内只执行一次。如果这个赋值发生得足够早,在 Hotspot 开始对 test
方法进行全面分析之前,Hotspot 永远不会注意到条件被采用并编译执行标量替换的代码。如果在执行条件时分析已经开始,Hotspot 将不会进行标量替换。使用 0xf9a0
的测试值,标量替换是否发生在我的计算机上是不确定的,因为分析开始的确切时间可能会有所不同(例如,因为分析和优化代码是在后台线程上编译的)。因此,如果我 运行 上面的变体有时会进行一些垃圾收集,有时不会。
Hotspot 的静态代码分析比 C/C++ 和其他静态编译器可以做的要有限得多,因此 Hotspot 在通过几个条件和其他控制结构来跟踪方法中的控制流方面不如智能确定变量引用的实例,即使它对于程序员或更聪明的编译器来说是静态可确定的。在许多情况下,分析信息可以弥补这一点,但需要注意这一点。
数组
如果数组的大小在 JIT 时间是已知的,则数组可以进行堆栈分配。但是,不支持对数组进行索引,除非 Hotspot 也可以在 JIT 时静态地确定索引值。所以堆栈分配的数组非常无用。由于大多数程序不直接使用数组而是使用标准集合,因此这不是很相关,因为嵌入对象(例如包含 ArrayList 中数据的数组)由于其嵌入性已经需要进行堆分配。我想这个限制的原因是不存在对局部变量的索引操作,所以这将需要额外的代码生成功能来处理一个非常罕见的用例。
从 Java 6 左右开始,Hotspot JVM 可以进行逃逸分析并在堆栈上而不是在垃圾收集堆上分配非逃逸对象。这会加快生成代码的速度并减少垃圾收集器的压力。
Hotspot可以堆栈分配对象的规则是什么?换句话说,我什么时候可以依靠它来进行堆栈分配?
编辑:这个问题是重复的,但是(IMO)下面的答案比原始问题的答案更好。
我做了一些实验以查看 Hotspot 何时能够进行堆栈分配。事实证明,它的堆栈分配比您基于 available documentation 所预期的要有限得多。 Choi "Escape Analysis for Java" 所引用的论文表明,仅分配给局部变量的对象始终可以进行堆栈分配。但事实并非如此。
所有这些都是当前 Hotspot 实现的实现细节,因此它们可能会在未来的版本中发生变化。这是指我的 OpenJDK 安装版本 1.8.0_121 for X86-64.
基于大量实验的简短总结似乎是:
如果
,Hotspot 可以堆栈分配一个对象实例- 它的所有用法都是内联的
- 它从不分配给任何静态或对象字段,只分配给局部变量
- 在程序的每一点,哪些局部变量包含对对象的引用必须是 JIT 时间可确定的,并且不依赖于任何不可预测的条件控制流。
- 如果对象是一个数组,它的大小在 JIT 时间必须是已知的并且索引到它必须使用 JIT 时间常量。
要了解这些条件何时成立,您需要对 Hotspot 的工作原理有相当多的了解。依赖 Hotspot 在特定情况下确定地进行堆栈分配可能会有风险,因为涉及到很多非本地因素。尤其是要知道是否所有内容都是内联的可能很难预测。
实际上,简单的迭代器通常是堆栈可分配的,如果你只是用它们来迭代的话。对于复合对象,只有外部对象可以进行堆栈分配,因此列表和其他集合总是会导致堆分配。
如果您有 HashMap<Integer,Something>
并在 myHashMap.get(42)
中使用它,42
可能会在测试程序中进行堆栈分配,但不会在完整的应用程序中使用,因为您可以确保整个程序中HashMaps中的key对象会多于两种类型,因此key上的hashCode和equals方法不会内联。
除此之外,我没有看到任何普遍适用的规则,这将取决于代码的具体情况。
热点内部结构
首先要知道的是逃逸分析是在内联之后执行的。这意味着 Hotspot 的逃逸分析在这方面比 Choi 论文中的描述更强大,因为从方法返回但对调用方方法而言是本地的对象仍然可以进行堆栈分配。因此,如果您这样做,迭代器几乎总是可以分配堆栈。 for(Foo item : myList) {...}
(myList.iterator()
的实现非常简单,通常如此。)
Hotspot 仅在确定方法是 'hot' 后才编译方法的优化版本,因此很多时候不是 运行 的代码根本不会得到优化,在这种情况下有没有任何堆栈分配或内联。但是对于那些你通常不关心的方法。
内联
内联决策基于 Hotspot 首先收集的分析数据。声明的类型并不重要,即使一个方法是虚拟的,Hotspot 也可以根据它在分析过程中看到的对象类型将其内联。分支(即 if 语句和其他控制流结构)也有类似的情况:如果在分析期间 Hotspot 从未看到某个分支被采用,它将基于该分支从未被采用的假设来编译和优化代码。在这两种情况下,如果 Hotspot 无法证明其假设始终为真,它将在编译后的代码中插入检查,称为 'uncommon traps',如果命中此类陷阱,Hotspot 将取消优化并可能重新优化采用新信息考虑在内。
Hotspot 将描述哪些对象类型作为接收者出现在哪些调用站点。如果 Hotspot 在调用点只看到一个类型或两个不同的类型,它能够内联被调用的方法。如果只有一两个非常常见的类型,而其他类型出现的频率要低得多,Hotspot 也应该仍然能够内联常见类型的方法,包括检查需要获取哪些代码。 (虽然我不完全确定最后一个案例有一个或两个常见类型和更不常见的类型)。如果有两种以上的常见类型,Hotspot 根本不会内联调用,而是生成机器码用于间接调用。
'Type' 这里指的是对象的确切类型。不考虑已实现的接口或共享的 super类。即使在一个调用点出现不同的接收者类型,但它们都继承了相同的方法实现(例如,多个 类 都从 Object
继承了 hashCode
),Hotspot 仍然会生成一个间接的打电话而不是内联。 (所以i.m.o。热点在这种情况下是相当愚蠢的。我希望未来的版本改进这一点。)
Hotspot 也只会内联不太大的方法。 'Not too big' 由 -XX:MaxInlineSize=n
和 -XX:FreqInlineSize=n
选项决定。 JVM 字节码大小低于 MaxInlineSize 的可内联方法始终被内联,如果调用是 'hot',则 JVM 字节码大小低于 FreqInlineSize 的方法被内联。较大的方法从不内联。默认情况下,MaxInlineSize 是 35,FreqInlineSize 是平台相关的,但对我来说是 325。所以如果你想要内联它们,请确保你的方法不会太大。有时它可以帮助从一个大方法中分离出公共路径,以便它可以内联到它的调用者中。
分析
关于分析需要了解的一件重要事情是,分析站点是基于 JVM 字节码的,它本身没有以任何方式内联。所以如果你有例如静态方法
static <T,U> List<U> map(List<T> list, Function<T,U> func) {
List<U> result = new ArrayList();
for(T item : list) { result.add(func.call(item)); }
return result;
}
将 SAM Function
可调用映射到列表和 returns 转换后的列表,Hotspot 会将对 func.call
的调用视为单个程序范围的调用站点。您可以在程序的多个位置调用此 map
函数,在每个调用点传递不同的函数(但一个调用点传递相同的函数)。在那种情况下,您可能希望 Hotspot 能够内联 map
,然后还可以调用 func.call
,因为每次使用 map
时,只有一个 func
类型.如果是这样,Hotspot 将能够非常紧密地优化循环。不幸的是,Hotspot 不够智能。它只为 func.call
调用站点保留一个配置文件,将您传递给 map
的所有 func
类型集中在一起。您可能会使用 func
的两个以上不同实现,因此 Hotspot 将无法内联对 func.call
的调用。 Link for more details, and archived link 因为原件似乎不见了。
(顺便说一句,在 Kotlin 中,等效循环可以完全内联,因为 Kotlin 编译器可以在字节码级别内联调用。因此对于某些用途,它可能比 Java.)
标量替换
另一件需要知道的重要事情是 Hotspot 实际上并没有实现 objects 的堆栈分配。相反,它实现了 标量替换 ,这意味着一个对象被解构为其组成字段,并且这些字段像普通局部变量一样分配堆栈。这意味着根本没有留下任何对象。标量替换仅在永远不需要创建指向堆栈分配对象的指针时才有效。某些形式的堆栈分配,例如C++ 或 Go 能够在堆栈上分配完整的对象,然后将指向它们的引用或指针传递给被调用的函数,但在 Hotspot 中这是行不通的。因此,如果需要将对象引用传递给非内联方法,即使引用不会逃逸被调用的方法,Hotspot 也会始终堆分配此类对象。
原则上 Hotspot 在这方面可以更聪明,但现在不是。
测试程序
我使用以下程序和变体来查看 Hotspot 何时进行标量替换。
// Minimal example for which the JVM does not scalarize the allocation. If field is final, or the second allocation is unconditional, it will.
class Scalarization {
int field = 0xbd;
long foo(long i) { return i * field; }
public static void main(String[] args) {
long result = 0;
for(long i=0; i<100; i++) {
result += test();
}
System.out.println("Result: "+result);
}
static long test() {
long ctr = 0x5;
for(long i=0; i<0x10000; i++) {
Scalarization s = new Scalarization();
ctr = s.foo(ctr);
if(i == 0) s = new Scalarization();
ctr = s.foo(ctr);
}
return ctr;
}
}
如果您使用 javac Scalarization.java; java -verbose:gc Scalarization
编译并 运行 这个程序,您可以看到标量替换是否根据垃圾收集次数起作用。如果标量替换有效,则我的系统上没有发生垃圾回收,如果标量替换无效,我会看到一些垃圾回收。
Hotspot 能够缩放的变体 运行 比它不能缩放的版本快得多。我验证了生成的机器代码 (instructions) 以确保 Hotspot 没有进行任何意外的优化。如果热点能够标量替换分配,那么它还可以对循环进行一些额外的优化,将其展开几次迭代,然后将这些迭代组合在一起。因此,在标量化版本中,有效循环计数较低,每次迭代都会执行多个源代码级迭代的工作。所以速度差异不仅仅是由于分配和垃圾收集开销。
观察结果
我尝试了上述程序的多种变体。标量替换的一个条件是对象绝不能分配给对象(或静态)字段,并且大概也不能分配给数组。所以在代码中
Foo f = new Foo();
bar.field = f;
Foo
对象不能被标量替换。即使 bar
本身被标量替换,并且如果您不再使用 bar.field
,这仍然成立。所以一个对象只能分配给局部变量。
仅此还不够,Hotspot 还必须能够在 JIT 时静态地确定哪个对象实例将成为调用的目标。例如,使用 foo
和 test
的以下实现并删除 field
会导致堆分配:
long foo(long i) { return i * 0xbb; }
static long test() {
long ctr = 0x5;
for(long i=0; i<0x10000; i++) {
Scalarization s = new Scalarization();
ctr = s.foo(ctr);
if(i == 50) s = new Scalarization();
ctr = s.foo(ctr);
}
return ctr;
}
而如果您随后删除第二次分配的条件,则不会再发生堆分配:
static long test() {
long ctr = 0x5;
for(long i=0; i<0x10000; i++) {
Scalarization s = new Scalarization();
ctr = s.foo(ctr);
s = new Scalarization();
ctr = s.foo(ctr);
}
return ctr;
}
在这种情况下,Hotspot 可以静态确定哪个实例是每次调用 s.foo
.
另一方面,即使对 s
的第二个赋值是 Scalarization
的子类且实现完全不同,只要赋值是无条件的,Hotspot 仍会标量分配。
Hotspot 似乎无法将对象移动到先前标量替换的堆中(至少在没有取消优化的情况下)。标量替换是一个孤注一掷的事情。所以在最初的 test
方法中,Scalarization
的两次分配总是发生在堆上。
条件
一个重要的细节是 Hotspot 将根据其分析数据预测条件。如果从未执行条件赋值,Hotspot 将在该假设下编译代码,然后可能能够进行标量替换。如果在稍后某个时间点确实采用了条件,Hotspot 将需要使用这个新假设重新编译代码。新代码不会进行标量替换,因为 Hotspot 无法再静态确定后续调用的接收者实例。
例如在 test
的这种变体中:
static long limit = 0;
static long test() {
long ctr = 0x5;
long i = limit;
limit += 0x10000;
for(; i<limit; i++) { // In this form if scalarization happens is nondeterministic: if the condition is hit before profiling starts scalarization happens, else not.
Scalarization s = new Scalarization();
ctr = s.foo(ctr);
if(i == 0xf9a0) s = new Scalarization();
ctr = s.foo(ctr);
}
return ctr;
}
条件赋值在程序的生命周期内只执行一次。如果这个赋值发生得足够早,在 Hotspot 开始对 test
方法进行全面分析之前,Hotspot 永远不会注意到条件被采用并编译执行标量替换的代码。如果在执行条件时分析已经开始,Hotspot 将不会进行标量替换。使用 0xf9a0
的测试值,标量替换是否发生在我的计算机上是不确定的,因为分析开始的确切时间可能会有所不同(例如,因为分析和优化代码是在后台线程上编译的)。因此,如果我 运行 上面的变体有时会进行一些垃圾收集,有时不会。
Hotspot 的静态代码分析比 C/C++ 和其他静态编译器可以做的要有限得多,因此 Hotspot 在通过几个条件和其他控制结构来跟踪方法中的控制流方面不如智能确定变量引用的实例,即使它对于程序员或更聪明的编译器来说是静态可确定的。在许多情况下,分析信息可以弥补这一点,但需要注意这一点。
数组
如果数组的大小在 JIT 时间是已知的,则数组可以进行堆栈分配。但是,不支持对数组进行索引,除非 Hotspot 也可以在 JIT 时静态地确定索引值。所以堆栈分配的数组非常无用。由于大多数程序不直接使用数组而是使用标准集合,因此这不是很相关,因为嵌入对象(例如包含 ArrayList 中数据的数组)由于其嵌入性已经需要进行堆分配。我想这个限制的原因是不存在对局部变量的索引操作,所以这将需要额外的代码生成功能来处理一个非常罕见的用例。