只是玩一些C#代码,发现扫描内存数组所需的时间取决于对象的大小。
让我解释一下,对于两个长度相同但对象大小不同的集合,大型对象的循环时间更长。
使用Linqpad进行测试:
SimpleObject
个对象的数组循环遍历所有需要~221 ms BigObject
个对象的数组循环遍历所有需要~756毫秒为什么时间不接近常数?它不应该使用kind of
指针算法吗?
由于
public class SmallObject{
public int JustAnInt0;
public static SmallObject[] FakeList(int size){
var res = new SmallObject[size];
for(var c = 0; c != size; ++c)
res[c] = new SmallObject();
return res;
}
}
public class MediumObject{
public int JustAnInt0;
public int JustAnInt1;
public int JustAnInt2;
public int JustAnInt3;
public int JustAnInt4;
public static MediumObject[] FakeList(int size){
var res = new MediumObject[size];
for(var c = 0; c != size; ++c)
res[c] = new MediumObject();
return res;
}
}
public class BigObject{
public int JustAnInt0;
public int JustAnInt1;
public int JustAnInt2;
public int JustAnInt3;
public int JustAnInt4;
public int JustAnInt5;
public int JustAnInt6;
public int JustAnInt7;
public int JustAnInt8;
public int JustAnInt9;
public int JustAnInt10;
public int JustAnInt11;
public int JustAnInt12;
public int JustAnInt13;
public int JustAnInt14;
public int JustAnInt15;
public int JustAnInt16;
public int JustAnInt17;
public int JustAnInt18;
public int JustAnInt19;
public static BigObject[] FakeList(int size){
var res = new BigObject[size];
for(var c = 0; c != size; ++c)
res[c] = new BigObject();
return res;
}
}
void Main()
{
var size = 30000000;
var small = SmallObject.FakeList(size);
var medium = MediumObject.FakeList(size);
var big = BigObject.FakeList(size);
var sw = System.Diagnostics.Stopwatch.StartNew();
for(var c = 0; c != size; ++c){
small[c].JustAnInt0++;
}
string.Format("Scan small list took {0}", sw.ElapsedMilliseconds).Dump();
sw.Restart();
for(var c = 0; c != size; ++c){
medium[c].JustAnInt0++;
}
string.Format("Scan medium list took {0}", sw.ElapsedMilliseconds).Dump();
sw.Restart();
for(var c = 0; c != size; ++c){
big[c].JustAnInt0++;
}
string.Format("Scan big list took {0}", sw.ElapsedMilliseconds).Dump();
}
// Define other methods and classes here
更新
在这种情况下,@ IanMercer评论,加上@erisco,以正确的方式指出了我,所以在调整了一些对象后,我得到了预期的行为。基本上我所做的是将额外的数据包装到一个对象中。通过这种方式,小型,中型和大型具有或多或少相同的大小,能够适应CPU高速缓存。现在测试显示同样的时间。
public class SmallObject{
public int JustAnInt0;
public static SmallObject[] FakeList(int size){
var res = new SmallObject[size];
for(var c = 0; c != size; ++c)
res[c] = new SmallObject();
return res;
}
}
public class MediumObject{
public int JustAnInt0;
public class Extra{
public int JustAnInt1;
public int JustAnInt2;
public int JustAnInt3;
public int JustAnInt4;
}
public Extra ExtraData;
public static MediumObject[] FakeList(int size){
var res = new MediumObject[size];
for(var c = 0; c != size; ++c)
res[c] = new MediumObject();
return res;
}
}
public class BigObject{
public int JustAnInt0;
public class Extra{
public int JustAnInt1;
public int JustAnInt2;
public int JustAnInt3;
public int JustAnInt4;
public int JustAnInt5;
public int JustAnInt6;
public int JustAnInt7;
public int JustAnInt8;
public int JustAnInt9;
public int JustAnInt10;
public int JustAnInt11;
public int JustAnInt12;
public int JustAnInt13;
public int JustAnInt14;
public int JustAnInt15;
public int JustAnInt16;
public int JustAnInt17;
public int JustAnInt18;
public int JustAnInt19;
}
public Extra ExtraData;
public static BigObject[] FakeList(int size){
var res = new BigObject[size];
for(var c = 0; c != size; ++c)
res[c] = new BigObject();
return res;
}
}
void Main()
{
var size = 30000000;
var small = SmallObject.FakeList(size);
var medium = MediumObject.FakeList(size);
var big = BigObject.FakeList(size);
var times = Enumerable
.Range(0, 10)
.Select(r => {
var sw = System.Diagnostics.Stopwatch.StartNew();
for(var c = 0; c != size; ++c){
small[c].JustAnInt0++;
}
// string.Format("Scan small list took {0}", sw.ElapsedMilliseconds).Dump();
var smalltt = sw.ElapsedMilliseconds;
sw.Restart();
for(var c = 0; c != size; ++c){
big[c].JustAnInt0++;
}
// string.Format("Scan big list took {0}", sw.ElapsedMilliseconds).Dump();
var bigtt = sw.ElapsedMilliseconds;
sw.Restart();
for(var c = 0; c != size; ++c){
medium[c].JustAnInt0++;
}
//string.Format("Scan medium list took {0}", sw.ElapsedMilliseconds).Dump();
var mediumtt = sw.ElapsedMilliseconds;
return new {
smalltt,
mediumtt,
bigtt
};
})
.ToArray();
(new {
Small = times.Average(t => t.smalltt),
Medium = times.Average(t => t.mediumtt),
Big = times.Average(t => t.bigtt)
}).Dump();
}
一些有用的链接:
谢谢大家!
答案 0 :(得分:5)
它不应该使用指针算法吗?
虽然CLR确实使用了"类指针算法"要在内存中找到项目,接下来会发生的事情是不同的:一旦开始访问JustAnInt0
,CLR就会开始从这些指针中读取数据。
这就是它变得混乱的地方:现代硬件针对缓存进行了大量优化,因此当您请求JustAnInt0
时,硬件预测JustAnInt1
,JustAnInt2
等将会跟随,因为对于大多数现实生活中的程序而言。这称为参考位置。与JustAnInt0
一起加载的项目数取决于硬件中缓存行的大小。当对象很小并且缓存行很大时,也可以加载相邻内存区域中的一个或两个对象。
当对象很小时,程序会无意中利用引用的局部性,因为当您访问small[c]
时,多个小对象会在缓存中结束。
此行为依赖于彼此相邻分配的小对象。如果您将随机随机播放应用于small
,medium
和big
,则访问时间应该更加紧密。
答案 1 :(得分:2)
我的回答是纯粹的推测,但希望它能提供一些测试和排除的东西。
public static SmallObject[] FakeList(int size){
var res = new SmallObject[size];
for(var c = 0; c != size; ++c)
res[c] = new SmallObject();
return res;
}
FakeList
一个接一个地分配许多对象并将它们存储在一个数组中。分配器将连续存储所有这些对象。在分代GC中,分配是通过指针碰撞完成的(没有搜索空闲空间)(read here)。
让我们说对象的开销是16 bytes。从中可以猜测SmallObject
的大小为20个字节,MediumObject
为36个字节,BigObject
为96个字节。
因此,我们有三个连续存储的对象数组。当CPU获取一个4字节的int时,它还会获取与int相邻的一堆内存(读取CPU cache and cache lines)。假设CPU一次提取64个字节。
有多少个对象适合缓存行?
0 20 40 60 84
| SmallObject | SmallObject | SmallObject | SmallObject |
0 36 72
| MediumObject | MediumObject |
0 96
| BigObject |
注意:我们在这里不考虑data alignment。
缓存行适合3.2 SmallObjects,1.77 MediumObjects和0.66 BigObjects。
我们在循环中递增JustAnInt0
,这恰好是对象的第一个字段。编译器可能按照你声明它们的顺序排列字段(因为它们都是整数,否则它可能会重新排序它们以用于内存对齐)。
考虑到这一点,假设JustAnInt0
在所有SmallObject,MediumObject和BigObject中都是字节16到20。这意味着我们可以一次从SmallObjects中获取3 JustAnInt0
,一次从MediumObject获取2 JustAnInt0
,一次只从BigObject获取1 JustAnInt0
。
因此,您可以在JustAnInt0
阵列上最快增加SmallObject
的原因是因为CPU可以立即将三个JustAnInt0
加载到其本地缓存中。这意味着与BigObject
相比,需要三分之一的主存储器访问。主内存访问比CPU缓存访问(read here)慢一个数量级,减少两个数量级。主内存访问是CPU最慢的指令之一,可能会占用算法的总时间成本。
同样,这完全是猜测。唯一真正了解的方法是了解您的硬件并进行一些测试。希望这为开始调查提供参考点。
答案 2 :(得分:0)
就像其他答案所说,它是因为CPU缓存和其他优化。
Smaller arrays: level 1 cache (very fast)
Larger arrays: level 2 cache (fast)
Huge arrays: not cached (normal)
Gigantic arrays: paged to disk (slow)
请参阅此simple explanation。