为什么当客户端在VS 2010中编译时,COM互操作层会慢40倍,而在VS 2005中不会?

31

我的团队使用一个大型模拟应用程序的COM API。大多数模拟文件都有几百兆字节大小,并且在打开时似乎会完全加载到内存中。

我们的主要任务是遍历文件对象模型中的所有元素,然后对每个元素执行“某些操作”。

我们最近将代码从VS 2005 .NET 2移植到了VS 2010 .NET 4,并发现迭代速度下降了约40倍(从约10秒变为约8分钟)。我们已将其简化为最小可能的代码示例(大约10行),在VS 2005中编译并运行,然后在VS 2010中打开项目并编译(我们正在使用制造商提供的COM互操作组件),同时保留框架为2。

在2005年测试应用程序完成需要10秒,在2010年需要8分钟。

这是什么原因?

更新

该代码等效于:

var server = new Server();
var elements = server.Elements;
var elementCount = elements.Count;

for(int i = 0; i < elementsCount; ++i)
{
    var element = elements[i];
}

这段代码在VS 2010中运行的时间比在VS 2005中慢40倍。

更新2

我推断造成一个版本比另一个版本运行速度显著下降的唯一原因是数据在不同的版本上通过COM传输方式存在差异。

我们记录了两种情况下的绑定日志,发现在快速版本中未找到CustomMarshalers的本机映像(这些是由FUSLOGVW捕获的绑定日志)。

mscorlib

mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089.HTM

快速版本

LOG: Start binding of native image mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089.
LOG: Start validating native image mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089.
WRN: Native image does not satisfy request. Looking for next native image.
WRN: No matching native image found.

缓慢

LOG: Start binding of native image mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089.
LOG: Start validating native image mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089.
LOG: Bind to native image succeeded.

自定义编组器

CustomMarshalers,版本=2.0.0.0,文化=中性,PublicKeyToken=b03f5f7f11d50a3a

快速

LOG: Start binding of native image CustomMarshalers, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a.
LOG: Start validating native image CustomMarshalers, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a.
WRN: Native image does not satisfy request. Looking for next native image.
WRN: No matching native image found.

缓慢

LOG: Start binding of native image CustomMarshalers, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a.
LOG: Start validating native image CustomMarshalers, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a.
LOG: Start validating all the dependencies.
LOG: [Level 1]Start validating native image dependency mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089.
LOG: Dependency evaluation succeeded.
LOG: [Level 1]Start validating IL dependency Microsoft.VisualC, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a.
LOG: Dependency evaluation succeeded.
LOG: Validation of dependencies succeeded.
LOG: Start loading all the dependencies into load context.
LOG: Loading of dependencies succeeded.
LOG: Bind to native image succeeded.
Native image has correct version information.
Attempting to use native image C:\WINDOWS\assembly\NativeImages_v2.0.50727_32\CustomMarshalers\3e6deccf191ab943d3a0812a38ab5c97\CustomMarshalers.ni.dll.
Native image successfully used.

看起来当不使用本地映像时,性能会得到很大提升。

为什么这个绑定会在一个情况下失败,在另一个情况下成功,我们如何强制应用程序不使用本地映像?

更新3

奇怪的事情还在继续。如果我在VS 2010中使用R#测试运行器或内置的Visual Studio测试运行器在一个测试方法中运行此代码,那么它会以快速的速度运行。

我已经尝试将此代码包装在程序集中,然后动态加载它,但没有任何区别。


在这里,"native image"并不是指未经管理的本地代码,而是指已经预编译并针对当前架构(在这种情况下是x86)进行了优化的托管程序集,而不是即时编译的代码。有趣的是,相反的情况正在发生:本地库的性能明显较差。 - satnhak
@Charleh 本地汇编是mscorlib核心框架库的一部分。不过我明天上班时会检查调试标志。 - satnhak
无论是否连接调试器,都不会有任何影响;它非常慢。然而,通过 MS Test 运行时运行时速度非常快。 - satnhak
你确定你的线程模型完全匹配吗?在调用方法的顶部加上[StaThread]可能会有所不同。此外,我曾经看到过使用自定义容器类来实现集合的情况,使用迭代器和索引集合之间存在巨大差异。 - user645280
感谢大家对此的帮助。原因似乎是即使在STA应用程序中,线程也是使用MTA线程模型创建的,这意味着COM对象实际上是在主线程上创建的,并且每个调用都在线程之间进行了封送(这是我的理解)。要解决这个问题,需要创建一个var t = new Thread()并设置t.SetApartmentState(ApartmentState.STA);默认情况下,所有线程都是MTA线程 - 你不能使用线程池(即bgworker),因为所有线程池线程都是MTA线程。 - satnhak
显示剩余19条评论
2个回答

6

这有点冒险。很高兴我能帮忙。

在调用任何COM对象时,匹配MTA与STA(线程模型)非常重要。在方法顶部使用[STAThread]指令是确保该方法中每个调用的线程模型的一种方式。

看起来Thread.SetApartmentState(ApartmentState.STA)适用于整个线程,但显然不适用于线程池线程。


非常感谢你。显然我还需要等两个小时才能授予悬赏,但一旦我可以,它就是你的了 :) - satnhak
如果你的后台线程调用一个被定义为[STAThread]的方法(并且循环在STA方法内部),那么你仍然应该能够看到性能提升。只需进入公寓一次,而不是像以前那样进入n次。 - user645280
我认为这也不完全正确。如果你的后台线程不是最初创建该对象的线程,无论它本身是否为STA,它都将通过代理进行处理。我认为你的答案不准确。 - cirrus
注意:经过进一步测试,[STAThread]仅适用于特定函数,如main()。例如在手动创建的线程中将无效,只有Thread.SetApartmentState()在这种情况下才能正常工作。 - user645280
1
但是为了清晰起见,仅使用SetApartmentState()是不够的。该对象还必须由该线程“拥有”-它必须首先创建它。仅仅将多个线程都标记为STA并尝试调用在其他线程上创建的STA对象(例如在线程池模型中)将会非常缓慢。 - cirrus

2
当你说“即使在STA应用程序中,线程也是...”时,这实际上是不正确的。一个线程可以选择在访问任何COM对象之前设置它的公寓状态,但是在.NET中,如果你什么都不做,这些线程将隐式地成为MTA。
线程池是MTA的。如果你考虑一下,它需要是MTA,因为如果它充满了STA线程,那么它将是一个糟糕的线程池,因为每当一个线程尝试访问在池中的其他线程上创建的对象时,它都需要进行封送。
Thread.SetApartmentState只能按定义对每个线程起作用。它永远不可能影响任何其他线程(正如你已经发现的那样)。对象属于一个公寓,一个线程可能属于一个单一的线程模型。如果线程尝试访问具有不匹配模型的对象,则需要进行封送。
如果你的COM服务器标记为“both”,那么你可以在STA或MTA线程中使用它而无需代理。如果是这种情况,你很幸运,应该在MTA线程上创建它(或让线程池线程这样做)。
如果你在STA线程上创建它,即使(尤其是)所有其他线程都是STA线程,它们也将全部通过代理进行访问,除非你恰好从最初创建它的线程中调用对象。
如果你的COM服务器是单线程的,那么你需要确保不仅从STA线程中调用它,而且还要从第一个创建它的STA线程中调用它,否则你将通过代理进行封送。

我不太明白你的意思。STA和线程之间存在1:1的关系。线程创建STA,由该线程创建的对象仅存在于该单元中,并且保证只能由该线程调用。如果线程调用CoInitializeEx(),它会创建一个与第一个STA不同的STA,对吗?线程必须运行消息泵来通过代理为其他线程提供消息请求服务。通常,您的主线程将具有消息泵,其他线程可能没有。基本上,如果您在任何其他线程上调用COM对象,无论是MTA还是STA,都将通过代理访问第一个对象。 - cirrus
我想说的是,如果其他线程使用STA线程模型,并且创建对象的线程相同,即使不是创建该类型对象的第一个线程,我也希望它们从性能方面受益。我很确定我以前见过这种情况,但我需要编写一个测试来确保。 - user645280
对于单线程 COM 对象来说,这似乎有一定道理。然而,当使用 Thread.SetApartmentState() 时,我发现公寓线程化的 COM 对象在子线程中获得性能优势(但是使用 [STAThread] 指令似乎没有效果)。 - user645280
如果您在STA线程1上创建了一个inproc COM服务器,并将其传递到STA线程2并从中使用它,那么您是否会看到相同的性能? - cirrus
这就是我所做的,访问在另一个线程中创建的对象非常缓慢(60,000个时钟周期),如果在您自己的线程中创建,则速度很快(50个时钟周期),如果您是MTA线程访问STA对象,则速度很慢(2,600个时钟周期)。 - user645280
显示剩余7条评论

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