gpt4 book ai didi

c# - C# 优化器性能差?

转载 作者:可可西里 更新时间:2023-11-01 08:47:27 25 4
gpt4 key购买 nike

我刚刚编写了一个小示例来检查 C# 优化器在索引器情况下的行为。这个例子很简单——我只是将一个数组包装在一个类中并尝试填充它的值:一次是直接填充,一次是通过索引器(它在内部访问数据的方式与直接解决方案完全相同)。

    public class ArrayWrapper
{
public ArrayWrapper(int newWidth, int newHeight)
{
width = newWidth;
height = newHeight;

data = new int[width * height];
}

public int this[int x, int y]
{
get
{
return data[y * width + x];
}
set
{
data[y * width + x] = value;
}
}

public readonly int width, height;
public readonly int[] data;
}

public class Program
{
public static void Main(string[] args)
{
ArrayWrapper bigArray = new ArrayWrapper(15000, 15000);

Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
for (int y = 0; y < bigArray.height; y++)
for (int x = 0; x < bigArray.width; x++)
bigArray.data[y * bigArray.width + x] = 12;
stopwatch.Stop();

Console.WriteLine(String.Format("Directly: {0} ms", stopwatch.ElapsedMilliseconds));

stopwatch.Restart();
for (int y = 0; y < bigArray.height; y++)
for (int x = 0; x < bigArray.width; x++)
bigArray[x, y] = 12;
stopwatch.Stop();

Console.WriteLine(String.Format("Via indexer: {0} ms", stopwatch.ElapsedMilliseconds));

Console.ReadKey();
}
}

许多 SO 帖子告诉我,程序员应该高度信任优化器来完成它的工作。但在这种情况下,结果非常令人惊讶:

Directly: 1282 ms
Via indexer: 2134 ms

(在启用优化的发布配置中编译,我仔细检查)。

这是一个巨大的差异 - 绝不是统计错误(而且它既可扩展又可重复)。

这是一个非常令人不快的惊喜:在这种情况下,我希望编译器内联索引器(它甚至不包括任何范围检查),但它没有这样做。这是反汇编(请注意,我的评论是对正在发生的事情的猜测):

直接

                    bigArray.data[y * bigArray.width + x] = 12;
000000a2 mov eax,dword ptr [ebp-3Ch] // Evaluate index of array
000000a5 mov eax,dword ptr [eax+4]
000000a8 mov edx,dword ptr [ebp-3Ch]
000000ab mov edx,dword ptr [edx+8]
000000ae imul edx,dword ptr [ebp-10h]
000000b2 add edx,dword ptr [ebp-14h] // ...until here
000000b5 cmp edx,dword ptr [eax+4] // Range checking
000000b8 jb 000000BF
000000ba call 6ED23CF5 // Throw IndexOutOfRange
000000bf mov dword ptr [eax+edx*4+8],0Ch // Assign value to array

通过索引器

                    bigArray[x, y] = 12;
0000015e push dword ptr [ebp-18h] // Push x and y
00000161 push 0Ch // (prepare parameters)
00000163 mov ecx,dword ptr [ebp-3Ch]
00000166 mov edx,dword ptr [ebp-1Ch]
00000169 cmp dword ptr [ecx],ecx
0000016b call dword ptr ds:[004B27DCh] // Call the indexer

(...)

data[y * width + x] = value;
00000000 push ebp
00000001 mov ebp,esp
00000003 sub esp,8
00000006 mov dword ptr [ebp-8],ecx
00000009 mov dword ptr [ebp-4],edx
0000000c cmp dword ptr ds:[004B171Ch],0 // Some additional checking, I guess?
00000013 je 0000001A
00000015 call 6ED24648
0000001a mov eax,dword ptr [ebp-8] // Evaluating index
0000001d mov eax,dword ptr [eax+4]
00000020 mov edx,dword ptr [ebp-8]
00000023 mov edx,dword ptr [edx+8]
00000026 imul edx,dword ptr [ebp+0Ch]
0000002a add edx,dword ptr [ebp-4] // ...until here
0000002d cmp edx,dword ptr [eax+4] // Range checking
00000030 jb 00000037
00000032 call 6ED23A5D // Throw IndexOutOfRange exception
00000037 mov ecx,dword ptr [ebp+8]
0000003a mov dword ptr [eax+edx*4+8],ecx // Actual assignment
}
0000003e nop
0000003f mov esp,ebp
00000041 pop ebp
00000042 ret 8 // Returning

这完全是一场灾难(就代码优化而言)。所以我的问题是:

  • 为什么这段代码(实际上很简单)没有得到适当的优化?
  • 如何修改此代码,使其按照我希望的方式进行优化(如果可能)?
  • 程序员能否像依赖 C++ 的优化器一样依赖 C# 的优化器?

好的,我知道,最后一个问题很难回答。但最近我阅读了很多关于 C++ 性能的问题,并且惊讶于优化器可以做多少(例如,std::tie 的总内联,两个 std::tuple 构造函数和动态重载 opeartor <)。


编辑:(回应评论)

看来,这实际上仍然是我的错,因为我在运行 IDE 时检查了性能。现在我从 IDE 中运行相同的程序,并通过调试器即时附加到它。现在我得到:

直接

                    bigArray.data[y * bigArray.width + x] = 12;
000000ae mov eax,dword ptr [ebp-10h]
000000b1 imul eax,edx
000000b4 add eax,ebx
000000b6 cmp eax,edi
000000b8 jae 000001FA
000000be mov dword ptr [ecx+eax*4+8],0Ch

索引器

                    bigArray[x, y] = 12;
0000016b mov eax,dword ptr [ebp-14h]
0000016e imul eax,edx
00000171 add eax,ebx
00000173 cmp eax,edi
00000175 jae 000001FA
0000017b mov dword ptr [ecx+eax*4+8],0Ch

这些代码完全相同(就 CPU 指令而言)。运行后,索引器版本取得了比直接版本更好的结果,但(我猜)只是因为缓存。将测试放入循环后,一切恢复正常:

Directly: 573 ms
Via indexer: 353 ms
Directly: 356 ms
Via indexer: 362 ms
Directly: 351 ms
Via indexer: 370 ms
Directly: 351 ms
Via indexer: 354 ms
Directly: 359 ms
Via indexer: 356 ms

嗯;学过的知识。 即使是在Release模式下编译,无论程序是在IDE中运行还是独立运行,还是有很大的区别。感谢@harold 的想法。

最佳答案

在立即附加调试器的情况下运行代码会生成缓慢的代码(除非您启用“抑制模块加载时的 JIT 优化”,但这会使调试变得有点困难)。我用来查看优化程序集的过程是抛出一个异常(有条件地,比如说,如果静态变量为 0,那么优化器不会太容易触发),并在它崩溃时附加调试器。您可能必须通过“手动选择调试器”路线。此外,确保启用“显示外部代码”(从调用堆栈的上下文菜单中)。

我得到的直接访问代码是

innerloop:
mov eax,dword ptr [esi+8] ; bigArray.width
imul eax,ebx ; * y
add eax,edi ; + x
mov edx,dword ptr [ebp-14h] ; pointer to bigArray.data
mov ecx,dword ptr [ebp-10h] ; \
cmp eax,ecx ; | bounds check
jae 00000087 ; /
mov dword ptr [edx+eax*4+8],0Ch ; data[index] = 12
inc edi ; x++
cmp edi,dword ptr [esi+8] ; \
jl innerloop ; / if (x < bigArray.width) goto innerloop

对于索引器:

innerloop:
mov eax,dword ptr [esi+8] ; bigArray.width
imul eax,ebx ; * y
add eax,edi ; + x
mov edx,dword ptr [ebp-14h] ; pointer to bigArray.data
mov ecx,dword ptr [ebp-10h] ; \
cmp eax,ecx ; | bounds check
jae 00000087 ; /
mov dword ptr [edx+eax*4+8],0Ch ; data[index] = 12
inc edi ; x++
cmp edi,dword ptr [esi+8] ; \
jl innerloop ; / if (x < bigArray.width) goto innerloop

这不是粘贴错误,内部循环的代码确实完全相同。

关于c# - C# 优化器性能差?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/17105773/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com