为什么这个循环 CLI 代码的性能没有明显快于 C# 托管代码?
Why is the performance of this looping CLI code not significantly faster than C# managed code?
当我注释掉下面的大部分代码并取消注释调用 CLI 代码的 SpeedCreateImageMap2D() 行时,秒表计时实际上是相同的(两者都约为 5 毫秒)。
我希望 CLI 代码比托管 C# 快很多(例如,5 倍到 10 倍),正如我过去在其他类似类型的循环函数中遇到的那样,但事实并非如此。
我是不是漏掉了什么?
更新 1:通过替换代码的顶部部分制作示例 minimum/complete/verifiable。
int width = 640;
int height = 512;
int numPixels = width * height;
ushort[] imageData = new ushort[numPixels];
for (int i = 0; i < numPixels; i++) {
imageData[i] = (ushort)randomGenerator.Next(4095);
}
Stopwatch sw = Stopwatch.StartNew();
// Create and populate a 2D pixel map
int rowNum, colNum;
ushort[,] pixelMap2D = new ushort[width, height];
for (int i = 0; i < numPixels; i++) {
rowNum = i / width;
colNum = i % width;
pixelMap2D[colNum, rowNum] = imageData[i];
}
//ushort[,] pixelMap2D = SpeedCode.SpeedClass.SpeedCreateImageMap2D(imageData, width, height);
Debug.WriteLine("Speed(ms): " + sw.Elapsed.TotalMilliseconds.ToString("N2"));
CLI 函数:
array<UInt16, 2> ^ SpeedClass::SpeedCreateImageMap2D(array<UInt16> ^imageData, int width, int height)
{
// Create and populate a 2D image map from a 1D array of image data
array<UInt16, 2> ^imageMap2D = gcnew array<UInt16, 2>(width, height);
int rowNum, colNum;
int numpixels = width * height;
for (int i = 0; i < numpixels; i++)
{
rowNum = i / width;
colNum = i % width;
imageMap2D[colNum, rowNum] = imageData[i];
}
return imageMap2D;
}
更新 2:根据建议将 CLI 代码更改为嵌套 for 循环可将性能提高约 2 倍,但相应的托管代码性能也提高了约 2 倍。如果有更快的方法,请告诉我。
array<UInt16, 2> ^ SpeedClass::SpeedCreateImageMap2D(array<UInt16> ^imageData, int width, int height)
{
// Create and populate a 2D image map from a 1D array of image data
array<UInt16, 2> ^imageMap2D = gcnew array<UInt16, 2>(width, height);
int k =0;
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
imageMap2D[i, j] = imageData[k];
k++;
}
}
return imageMap2D;
}
C# 和托管 C++ 都编译为 IL,而不是在运行时进行 JIT。由于两个版本的结果开销大致相同,并且代码本身可能被编译成非常相似的 IL - 因此预计速度不会有显着差异。
您将从常规 C++/C 代码(不需要 JIT)和更少的支持库中获得更快的启动。
由于更好的优化,您可能使用 C++/C 获得更好的代码本身性能,但您的代码非常简单并且可能通过常规 JIT 生成近乎最佳的本机代码C# 生成的代码。或者由于内存管理的不同折衷,本机代码可能会变慢(托管代码几乎没有时间分配和相对昂贵的取消分配,而本机代码通常平均分配成本)。
I expected the CLI code to be much (e.g., 5x-10x) faster than the managed C# as I have experienced on other similar kinds of looping functions in the past
这非常依赖于工作量。它还取决于您是 VC++ 将代码编译为 IL 还是本机代码。
在这里,我猜测 除法和模数 主导性能。虽然 +-&|^
等其他原始操作非常快(如 1 CPU 周期延迟),但除法非常昂贵(即使在现代 CPUs 上也需要 15-30 个周期)。这些操作支配着吞吐量。其他一切都无关紧要。 (有趣的事实:使用 %
计算桶的哈希表比其他方法慢得多!这个操作真的很慢。)。
寻找更好的方法来计算 rowNum
和 colNum
。两个嵌套循环是一种常见的方法。
程序员往往会忽视他们最有效的可用优化器,即他们耳朵之间的优化器。你犯了几个错误:
- .NET 中的多维数组效率很低。索引它们很慢,它需要乘以较低的维度大小,并且有 rank 次元素访问的边界检查次数。锯齿状数组 好得多 ,索引简单,只需指针+大小计算和一次边界检查。
- 您的代码以对缓存非常不友好的顺序寻址数组。引用的位置在现代处理器上非常重要,您总是希望按存储顺序寻址内存。
- C++/CLI 编译器为使用 /clr 编译的代码生成 MSIL。即使是纯原生 C++ 代码(这不是),您仍然依赖抖动来生成和优化机器代码。这里的 C# 编译器生成的 MSIL 类型没有任何区别。这就是为什么您无法观察到任何差异的原因。
- 编写比抖动更快的本机代码并不容易。通常只有当你故意使代码不安全时,你才会领先,例如绕过数组索引边界检查。但是很难在这样的代码上获得回报,这里真正的节流阀是内存总线。不适合处理器高速缓存的大型阵列会导致太多停顿,除了升级硬件之外,您无能为力。
具有以下要点的相同代码的另一个版本:
static array<array<UInt16>^>^ CreateImageMap2D(array<UInt16>^ imageData, int width, int height) {
// Create and populate a 2D image map from a 1D array of image data
auto imageMap2D = gcnew array<array<UInt16>^>(height);
int k = 0;
for (int i = 0; i < height; i++) {
imageMap2D[i] = gcnew array<UInt16>(width);
Array::Copy(imageData, k, imageMap2D[i], 0, width);
k += width;
}
return imageMap2D;
}
我没有测量它,但您应该在更好的缓存利用率方面遥遥领先。进一步优化此代码不太可能获得回报,您需要击败 Array::Copy()。它已经被优化了。但是您可以尝试使用 pin_ptr<>
来固定数组和 memcpy() 来复制数据。用 C# 写这个不会有任何区别,可能是你想做的。
当我注释掉下面的大部分代码并取消注释调用 CLI 代码的 SpeedCreateImageMap2D() 行时,秒表计时实际上是相同的(两者都约为 5 毫秒)。
我希望 CLI 代码比托管 C# 快很多(例如,5 倍到 10 倍),正如我过去在其他类似类型的循环函数中遇到的那样,但事实并非如此。
我是不是漏掉了什么?
更新 1:通过替换代码的顶部部分制作示例 minimum/complete/verifiable。
int width = 640;
int height = 512;
int numPixels = width * height;
ushort[] imageData = new ushort[numPixels];
for (int i = 0; i < numPixels; i++) {
imageData[i] = (ushort)randomGenerator.Next(4095);
}
Stopwatch sw = Stopwatch.StartNew();
// Create and populate a 2D pixel map
int rowNum, colNum;
ushort[,] pixelMap2D = new ushort[width, height];
for (int i = 0; i < numPixels; i++) {
rowNum = i / width;
colNum = i % width;
pixelMap2D[colNum, rowNum] = imageData[i];
}
//ushort[,] pixelMap2D = SpeedCode.SpeedClass.SpeedCreateImageMap2D(imageData, width, height);
Debug.WriteLine("Speed(ms): " + sw.Elapsed.TotalMilliseconds.ToString("N2"));
CLI 函数:
array<UInt16, 2> ^ SpeedClass::SpeedCreateImageMap2D(array<UInt16> ^imageData, int width, int height)
{
// Create and populate a 2D image map from a 1D array of image data
array<UInt16, 2> ^imageMap2D = gcnew array<UInt16, 2>(width, height);
int rowNum, colNum;
int numpixels = width * height;
for (int i = 0; i < numpixels; i++)
{
rowNum = i / width;
colNum = i % width;
imageMap2D[colNum, rowNum] = imageData[i];
}
return imageMap2D;
}
更新 2:根据建议将 CLI 代码更改为嵌套 for 循环可将性能提高约 2 倍,但相应的托管代码性能也提高了约 2 倍。如果有更快的方法,请告诉我。
array<UInt16, 2> ^ SpeedClass::SpeedCreateImageMap2D(array<UInt16> ^imageData, int width, int height)
{
// Create and populate a 2D image map from a 1D array of image data
array<UInt16, 2> ^imageMap2D = gcnew array<UInt16, 2>(width, height);
int k =0;
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
imageMap2D[i, j] = imageData[k];
k++;
}
}
return imageMap2D;
}
C# 和托管 C++ 都编译为 IL,而不是在运行时进行 JIT。由于两个版本的结果开销大致相同,并且代码本身可能被编译成非常相似的 IL - 因此预计速度不会有显着差异。
您将从常规 C++/C 代码(不需要 JIT)和更少的支持库中获得更快的启动。
由于更好的优化,您可能使用 C++/C 获得更好的代码本身性能,但您的代码非常简单并且可能通过常规 JIT 生成近乎最佳的本机代码C# 生成的代码。或者由于内存管理的不同折衷,本机代码可能会变慢(托管代码几乎没有时间分配和相对昂贵的取消分配,而本机代码通常平均分配成本)。
I expected the CLI code to be much (e.g., 5x-10x) faster than the managed C# as I have experienced on other similar kinds of looping functions in the past
这非常依赖于工作量。它还取决于您是 VC++ 将代码编译为 IL 还是本机代码。
在这里,我猜测 除法和模数 主导性能。虽然 +-&|^
等其他原始操作非常快(如 1 CPU 周期延迟),但除法非常昂贵(即使在现代 CPUs 上也需要 15-30 个周期)。这些操作支配着吞吐量。其他一切都无关紧要。 (有趣的事实:使用 %
计算桶的哈希表比其他方法慢得多!这个操作真的很慢。)。
寻找更好的方法来计算 rowNum
和 colNum
。两个嵌套循环是一种常见的方法。
程序员往往会忽视他们最有效的可用优化器,即他们耳朵之间的优化器。你犯了几个错误:
- .NET 中的多维数组效率很低。索引它们很慢,它需要乘以较低的维度大小,并且有 rank 次元素访问的边界检查次数。锯齿状数组 好得多 ,索引简单,只需指针+大小计算和一次边界检查。
- 您的代码以对缓存非常不友好的顺序寻址数组。引用的位置在现代处理器上非常重要,您总是希望按存储顺序寻址内存。
- C++/CLI 编译器为使用 /clr 编译的代码生成 MSIL。即使是纯原生 C++ 代码(这不是),您仍然依赖抖动来生成和优化机器代码。这里的 C# 编译器生成的 MSIL 类型没有任何区别。这就是为什么您无法观察到任何差异的原因。
- 编写比抖动更快的本机代码并不容易。通常只有当你故意使代码不安全时,你才会领先,例如绕过数组索引边界检查。但是很难在这样的代码上获得回报,这里真正的节流阀是内存总线。不适合处理器高速缓存的大型阵列会导致太多停顿,除了升级硬件之外,您无能为力。
具有以下要点的相同代码的另一个版本:
static array<array<UInt16>^>^ CreateImageMap2D(array<UInt16>^ imageData, int width, int height) {
// Create and populate a 2D image map from a 1D array of image data
auto imageMap2D = gcnew array<array<UInt16>^>(height);
int k = 0;
for (int i = 0; i < height; i++) {
imageMap2D[i] = gcnew array<UInt16>(width);
Array::Copy(imageData, k, imageMap2D[i], 0, width);
k += width;
}
return imageMap2D;
}
我没有测量它,但您应该在更好的缓存利用率方面遥遥领先。进一步优化此代码不太可能获得回报,您需要击败 Array::Copy()。它已经被优化了。但是您可以尝试使用 pin_ptr<>
来固定数组和 memcpy() 来复制数据。用 C# 写这个不会有任何区别,可能是你想做的。