使用调试器时C#代码非常缓慢;是MemoryMappedFile的问题吗?

11

我有一个客户端/服务器应用程序。 服务器组件以“远程”方式运行,使用WCF(二进制格式化程序,会话对象)。

如果我启动服务器组件并启动客户端,服务器完成的第一个任务需时<0.5秒钟。

如果我使用VS调试器附加并启动客户端,则该任务需要高达20秒钟才能完成。

没有代码更改-没有条件编译更改。无论服务器组件编译并在32位、64位下运行,是否使用VS托管过程,没有使用VS托管过程还是任何这些事情的组合,情况都是相同的。

可能很重要:如果我使用VS.NET profiler(采样模式),则应用程序的运行速度与未附加调试器的速度一样快。所以我不能通过这种方式诊断它。刚才检查了,插装模式也运行得很快。并发分析模式也是如此。

关键数据:

  • 应用程序使用相当多的多线程(标准线程池中有40个线程)。创建线程发生得很快,而且不是慢点。有很多锁、WaitHandleMonitor模式
  • 应用程序不会引发任何异常。
  • 应用程序不会创建控制台输出。
  • 应用程序完全由托管代码组成。
  • 应用程序将一些磁盘文件映射到MemoryMappedFile:1x750MB和12x8MB以及一些较小的文件

测量性能:

  • 在这两种情况下,CPU使用率很低;当调试器附加时,CPU保持在<1%
  • 在这两种情况下,内存使用率很低;可能为50或60MB
  • 有大量页面故障(参考MMF),但是当附加调试器时,它们发生得更慢
如果没有使用VS托管进程,或者基本上启用了'远程调试监视器',那么会使用相当多的CPU,并创建大量页面故障。但这不是唯一发生问题的时候。
无论客户端如何运行,都会看到性能差异。唯一更改的变量是通过“启动调试”运行的服务器组件与从资源管理器启动的组件有所不同。
我的想法:
  • WCF在调试时很慢吗?
  • MemoryMappedFiles在调试时很慢吗?
  • 使用40个线程- 调试缓慢?也许监视器/锁通知调试器?线程调度变得奇怪/上下文切换非常不频繁?
  • 宇宙背景辐射授予VS智慧和残酷的幽默感
所有这些看起来都极不可能。
所以,我的问题是:
  1. 为什么会出现这种情况?
  2. 如果第一个问题未知,则如何诊断/找出原因?

1
你是否启用了第一次异常捕获?您还可以尝试启用.NET服务器源代码步进以在调试模式下捕获最多的底层“隐藏”异常,特别是(反)序列化异常。另外,跟踪信息呢(outputdebugstring或其他)? - Simon Mourier
来自WCF的异常:“字符' ',十六进制值为0x20,不能包含在名称中。”我从未想到异常可以以这种方式隐藏:难道异常不是异常吗?我会尽力解决。也许你可以发布一个答案,如果这个解决方案有效,你就可以得到一些赞/an accept? :) - Kieren Johnstone
3
你是否在使用条件断点?我曾经看到过这种做法会极大地减慢工作速度。 - Paul Phillips
@Paul - 没有断点!我认为Simon正在尝试修复隐藏的异常,看看是否可以加快一切。 - Kieren Johnstone
@PaulPhillips - 你应该把那个放在答案里,它解决了我的问题。 - Scott Langham
显示剩余4条评论
3个回答

11

由于这个问题是谷歌搜索结果中的首要之一,我想在这里添加我的问题解决方案,希望能像我一样,为其他人节省2小时的调查研究时间。

在没有调试器附加的情况下,我的代码执行时间为30秒,但在使用调试器后变成了4分钟。原因是由于我忘记删除条件断点,这些断点会极大地减缓执行速度,所以请注意。


9

异常会显著影响应用程序的性能。有两种类型的异常:第一次机会异常(通过try/catch块优雅处理的异常)和未处理异常(最终会导致应用程序崩溃)。

默认情况下,调试器不显示第一次机会异常,它只显示未处理异常。并且默认情况下,它仅显示在您的代码中发生的异常。但是,即使它不显示它们,它仍然处理它们,因此其性能可能会受到影响(特别是在负载测试或大型循环运行中)。

要在Visual Studio中启用第一次机会异常显示,请单击“调试|异常”以调用异常对话框,并在“公共语言运行时”部分上选中“抛出”(您可以更具体地选择要查看的第一次机会异常)。

要启用来自应用程序任何位置的第一次机会异常显示,而不仅仅是来自您的代码,请单击“工具|选项|调试|常规”,并禁用“启用仅限我的代码”选项。

对于这些特定的“取证模式”案例,我强烈建议启用.NET Framework源代码调试(需要禁用“仅调试我的代码”)。了解发生了什么非常有用,有时仅查看调用堆栈就可以获得启示 - 尤其是在宇宙辐射混淆的情况下非常有帮助 :-)
两篇相关的有趣文章:
- 如何调试崩溃和挂起 - 配置Visual Studio以调试.NET Framework源代码

1
根据我的其他评论,直到我启用了“.NET Framework源代码调试”,异常才完全“隐藏”起来。存在一个错误,即SerializationInfo.SetValue()引发异常,指出“string”参数不是有效的XML元素名称,尽管它仍然可以继续工作,而且我正在使用NetTcpBinding(即二进制格式化程序)。 - Kieren Johnstone
大多数指令在VS2019上已经不存在了。希望能对这个答案进行编辑 :) - Gulzar
@Gulzar - 我不确定该如何处理,因为当您使用现代工具时,这种答案有点过时,但并非每个人都这样做。此外,一些信息仍然有效。通常,像您这样的人会查看日期并发现它已过时,所以没什么大不了的。在我看来,您应该再写一个问题或者给出另一个答案。 - Simon Mourier
@SimonMourier 我实际上在尝试之后才看日期。Tyron的答案最终成为了解决方案。谢谢 :) - Gulzar

0
可能原因:
各种特殊类型的断点,例如: - 条件断点 - 内存更改断点 - 函数断点
勾选“启用本机代码调试”选项。 - 此选项会使调试运行缓慢。
过度使用。 - 我的基准测试显示,当没有调试运行时,1000次的调用仅花费10毫秒,但是在进行调试时需要整整500毫秒。显然,Visual Studio调试器在活动时会拦截DotNet调试输出,并且对其进行极耗时的操作。(在使用Microsoft产品的几十年中,我们已经习惯了Microsoft的此种做法。) - 用kernel32.dll-> PInvoke->替换也无济于事,因为当运行DotNet应用程序时,Visual Studio完全忽略kernel32的调试输出,只显示DotNet的调试输出,这是一种完全不同的东西。(我想,任何其他东西再次都会太有意义了。)
过多抛出和捕获异常,正如另一个答案所建议的那样。 - 在DotNet下抛出异常是一种极其缓慢的操作。 - 在DotNet下收集堆栈跟踪是一种疯狂缓慢的操作。

事实上,当调试器附加时,使用System.Diagnostic.Trace进行写入非常缓慢,特别是在远程调试(例如进入Azure应用程序服务)时。我曾经遇到一个恶性循环,因为每次函数变慢时都会记录到这里,从而使它变得更慢,并导致调用函数中的相同跟踪逻辑再次记录日志等等。异步触发似乎可以正常工作。 - Dylan Nicholson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接