当与垃圾收集语言一起使用时,哪种代码更 CPU/memory 高效?
Which code is more CPU/memory efficient when used with a Garbage Collected language?
我有这两段虚拟代码(假设它们是用 Java 或 C# 编写的,所有变量都是本地的):
代码 1:
int a;
int b = 0;
for (int i = 1; i < 10 ; i++)
{
a = 10;
b += i;
// a lot of more code that doesn't involve assigning new values to "a"
}
代码 2:
int b = 0;
for (int i = 1; i < 10 ; i++)
{
int a = 10;
b += i;
// a lot of more code that doesn't involve assigning new values to "a"
}
乍一看,我会说这两个代码消耗相同数量的内存,但代码 1 更 CPU 高效,因为它只创建和分配变量 a
一次。
然后我读到垃圾收集器非常高效,以至于代码 2 的内存(和 CPU?)效率更高:将变量 a
保留在循环中使其属于 Gen0,因此它会在变量 b
.
之前被垃圾收集
因此,当与垃圾收集语言一起使用时,代码 2 的效率更高。我说的对吗?
在片段 2 中的代码实际执行之前,它最终将被转换为看起来像片段 1 中的代码在幕后(无论是编译器还是运行时)。因此,这两个代码片段的性能将是相同的,因为它们会在某个时刻编译成功能相同的代码。
请注意,对于非常短暂的变量,实际上很可能根本没有为它们分配内存。它们很可能完全存储在寄存器中,涉及 0 内存分配。
几点:
int(和其他原语)永远不会在堆上分配。它们直接存在于线程堆栈中,"allocation" 和 "deallocation" 是指针的简单移动,并且发生一次(当函数进入时,紧接在 return 之后),与作用域无关。
经常访问的基元通常存储在寄存器中以提高速度,同样,无论范围如何。
在你的情况下 a
(可能 b
以及整个循环)将是 "optimized away",优化器足够聪明检测变量值发生变化但从未被读取的情况,并跳过冗余操作。或者,如果有代码实际查看 a
,但不修改它,它可能会被优化器替换为常量值“10”,它只会在 [=10] 的任何地方内联显示=] 被引用。
New objects(如果你做了类似 String a = new String("foo")
的事情而不是 int)是 always 分配在年轻一代, 并且只有在他们活过几个小 collections 之后才会被转移到老一代。这意味着,在大多数情况下,当一个 object 被分配在函数内部,并且从不从外部引用时,无论其确切范围如何,它都永远不会进入老一代,除非你的堆结构迫切需要调整。
正如评论中指出的那样,有时 VM 可能会决定直接在老一代中分配一个大的 object(java 也是如此,不仅仅是.net),所以上面的观点只适用于大多数情况,而不是always。然而,关于这个问题,这没有任何区别,因为如果决定在老一代中分配一个 object,无论如何它都是在不考虑其初始引用范围的情况下做出的。
从性能和内存的角度来看,您的两个代码段是相同的。但是从可读性的角度来看,在尽可能窄的范围内声明所有变量总是一个好主意。
我有这两段虚拟代码(假设它们是用 Java 或 C# 编写的,所有变量都是本地的):
代码 1:
int a;
int b = 0;
for (int i = 1; i < 10 ; i++)
{
a = 10;
b += i;
// a lot of more code that doesn't involve assigning new values to "a"
}
代码 2:
int b = 0;
for (int i = 1; i < 10 ; i++)
{
int a = 10;
b += i;
// a lot of more code that doesn't involve assigning new values to "a"
}
乍一看,我会说这两个代码消耗相同数量的内存,但代码 1 更 CPU 高效,因为它只创建和分配变量 a
一次。
然后我读到垃圾收集器非常高效,以至于代码 2 的内存(和 CPU?)效率更高:将变量 a
保留在循环中使其属于 Gen0,因此它会在变量 b
.
因此,当与垃圾收集语言一起使用时,代码 2 的效率更高。我说的对吗?
在片段 2 中的代码实际执行之前,它最终将被转换为看起来像片段 1 中的代码在幕后(无论是编译器还是运行时)。因此,这两个代码片段的性能将是相同的,因为它们会在某个时刻编译成功能相同的代码。
请注意,对于非常短暂的变量,实际上很可能根本没有为它们分配内存。它们很可能完全存储在寄存器中,涉及 0 内存分配。
几点:
int(和其他原语)永远不会在堆上分配。它们直接存在于线程堆栈中,"allocation" 和 "deallocation" 是指针的简单移动,并且发生一次(当函数进入时,紧接在 return 之后),与作用域无关。
经常访问的基元通常存储在寄存器中以提高速度,同样,无论范围如何。
在你的情况下
a
(可能b
以及整个循环)将是 "optimized away",优化器足够聪明检测变量值发生变化但从未被读取的情况,并跳过冗余操作。或者,如果有代码实际查看a
,但不修改它,它可能会被优化器替换为常量值“10”,它只会在 [=10] 的任何地方内联显示=] 被引用。New objects(如果你做了类似
String a = new String("foo")
的事情而不是 int)是 always 分配在年轻一代, 并且只有在他们活过几个小 collections 之后才会被转移到老一代。这意味着,在大多数情况下,当一个 object 被分配在函数内部,并且从不从外部引用时,无论其确切范围如何,它都永远不会进入老一代,除非你的堆结构迫切需要调整。正如评论中指出的那样,有时 VM 可能会决定直接在老一代中分配一个大的 object(java 也是如此,不仅仅是.net),所以上面的观点只适用于大多数情况,而不是always。然而,关于这个问题,这没有任何区别,因为如果决定在老一代中分配一个 object,无论如何它都是在不考虑其初始引用范围的情况下做出的。
从性能和内存的角度来看,您的两个代码段是相同的。但是从可读性的角度来看,在尽可能窄的范围内声明所有变量总是一个好主意。