我的32位头疼现在变成了64位的偏头痛?!?(或者是64位.NET CLR运行时问题)

9
当从64位JIT切换到32位JIT时,运行.NET应用程序在性能、内存等方面出现了哪些不寻常、意外的后果?我对好的结果感兴趣,但更关心人们遇到的令人惊讶的坏问题。
我正在编写一个新的.NET应用程序,将在32位和64位上部署。有许多与移植应用程序相关的问题 - 我不关心从编程/移植角度来看的陷阱。 (即:正确处理本机/COM互操作,嵌入在结构中的引用类型改变结构的大小等)
然而,这个问题及其答案让我思考 - 我还忽视了哪些问题?
有许多问题和博客文章绕过这个问题,或者只涉及其中一个方面,但我没有看到任何编译出一份体面问题列表的东西。

特别是 - 我的应用程序非常CPU密集且具有巨大的内存使用模式(因此首先需要64位),同时又是图形化的。我担心CLR或JIT在64位Windows上运行时可能存在其他隐藏问题 (使用.NET 3.5sp1)。

以下是我目前知道的一些问题:

我想知道人们在64位Windows上的JIT发现了哪些其他具体的问题,并且是否有任何性能方面的解决方法。

谢谢大家!

----编辑-----

仅作澄清 -

我知道过早优化通常是不好的。我知道对系统进行第二次考虑通常是不好的。我也知道适用于64位的可移植性有其自身的问题 - 我们每天在64位系统上运行和测试以帮助解决这个问题等。

然而,我的应用程序并不是您典型的商业应用程序。它是一个科学软件应用程序。我们有许多进程在所有核心上使用100%的CPU(高度线程化)长达数小时。

我花费了很多时间对应用程序进行分析,这使得巨大的差异。但是,大多数分析器禁用JIT的许多功能,因此在分析器下运行时,例如内存分配,JIT中的内联等细节可能非常难以确定。因此我需要提出这个问题。


如果标题提到了.NET 32位和64位运行时,那么这个线程会更有用(易于在Google或Stacko-search等搜索引擎中找到)。 - easeout
8个回答

4

.NET中一个特别让人头疼的性能问题与JIT有关:

https://connect.microsoft.com/VisualStudio/feedback/details/93858/struct-methods-should-be-inlined?wa=wsignin1.0

基本上,在x64下,内联和结构体无法很好地协同工作(尽管该页面表明现在内联已经可以使用但随后的冗余副本并未被消除,这听起来有些可疑,因为性能差异微不足道)。

无论如何,经过长时间与.NET的斗争,我的解决方案是对于任何数字密集型任务都使用C++。即使在.NET的“良好”情况下,您没有处理结构体并且使用了优化掉边界检查的数组,C++也能轻松击败.NET

如果您正在做比点积更复杂的事情,则情况会非常糟糕;.NET代码既更长+不易读(因为您需要手动内联一些内容和/或无法使用泛型),而且速度也慢得多。

我已经转而使用C++中的Eigen:它非常棒,可以生成易读的代码并实现高性能;然后是一个薄薄的C++/CLI包装器,提供计算引擎与.NET世界之间的粘合剂。

Eigen通过模板元编程工作;在编译时,将向量表达式编译成SSE内部指令,并为您处理一些最恶劣的缓存相关循环展开和重排;尽管专注于线性代数,但它也可以与整数和非矩阵数组表达式一起使用。

因此,例如,如果P是一个矩阵,则这种类型的内容就可以轻松完成:

1.0 /  (P.transpose() * P).diagonal().sum();

这段代码不会分配临时的转置P变量,也不会计算整个矩阵乘积,而只计算所需的字段。

因此,如果您可以在完全信任的情况下运行,请使用C++/CLI,它的效果要好得多。


3

我记得在我经常访问的IRC频道上听到一个问题。在这种情况下,它会优化掉临时副本:

EventHandler temp = SomeEvent;
if(temp != null)
{
    temp(this, EventArgs.Empty);
}

将竞态条件重新引入并导致潜在的空引用异常。

有趣的是...这种优化只在64位JIT上发生,还是32位JIT上也会发生? - Reed Copsey
不会发生在32位。这不是我的对话,所以我无法验证这一点,但对话持续了一个多小时,除非有其他64位抖动,否则它很可能是你正在处理的那个。 - Quibblesome
如果我没记错,32位Jitter实际上在这种情况下并不符合规范,而且应该以这种方式进行优化。但这是一种防止在不同线程中触发事件和取消挂钩时出现竞争条件的技巧。 - Quibblesome
2
这个问题只存在于.NET 1.x在x64上;自从引入了.NET 2.0内存模型以来,这个问题就不再是一个问题了;请参见http://code.logos.com/blog/2008/11/events_and_threads_part_4.html和http://msdn.microsoft.com/magazine/cc163715.aspx。 - Bradley Grainger

1
关于Quibblesome的回答:
我尝试在我的Windows 7 x64上以Release模式而不使用调试器运行以下代码,但从未抛出NullReferenceException。
using System;
using System.Threading;

namespace EventsMultithreadingTest
{
    public class Program
    {
        private static Action<object> _delegate = new Action<object>(Program_Event);
        public static event Action<object> Event;

        public static void Main(string[] args)
        {
            Thread thread = new Thread(delegate()
                {
                    while (true)
                    {
                        Action<object> ev = Event;

                        if (ev != null)
                        {
                            ev.Invoke(null);
                        }
                    }
                });
            thread.Start();

            while (true)
            {
                Event += _delegate;
                Event -= _delegate;
            }
        }

        static void Program_Event(object obj)
        {
            object.Equals(null, null);
        }
    }
}

2
这个问题只存在于.NET 1.x在x64上;自从2005年引入了.NET 2.0内存模型以来,这个问题就不再是一个问题了;请参见http://code.logos.com/blog/2008/11/events_and_threads_part_4.html和http://msdn.microsoft.com/magazine/cc163715.aspx。 - Bradley Grainger

1

大多数情况下,Visual Studio和编译器都能很好地隐藏问题。然而,我知道一个主要的问题,如果您将应用程序设置为自动检测平台(x86 vs x64)并且依赖于32位第三方dll,则可能会出现问题。在这种情况下,在64位平台上,它将尝试使用64位约定和结构调用dlls,但它不起作用。


是的 - 我并不太关心这些类型的问题。我更关注隐藏在性能/内存/运行时等问题中的陷阱。 - Reed Copsey
+1 - 我在使用第三方库时遇到了这个问题。我必须在我的安装程序中包含32位和64位版本,并安装适当的版本。 - TWA

1
你提到了移植问题,这些是需要关注的。我(显然)不知道你的应用程序,但试图猜测JIT通常是完全浪费时间的。编写JIT的人对x86/x64芯片架构有着深入的了解,很可能比地球上其他任何人都知道什么性能更好,什么性能更差。
是的,你可能有一个不同和独特的特例,但如果你正在“编写一个新的应用程序”,那么我不会担心JIT编译器。很可能有一个可以避免的愚蠢循环,它将为你带来比试图猜测JIT获得的性能改进100倍以上。这让我想起我们在编写ORM时遇到的问题,我们会看着代码,认为我们可以从中调整几个机器指令...当然,代码随后会通过网络连接到数据库服务器,所以我们只是在一个被毫秒限制的过程中削减微秒。
性能调优的普遍规则是...如果你没有测量过性能,你不知道你的瓶颈在哪里,你只是认为你知道...而且你很可能是错的。

Walden: 我同意。然而,我的应用程序非常CPU密集型。它涉及高度数学计算,并且有许多需要运行数小时的进程。我花费大量时间对细节进行分析和优化,这可以极大地帮助提升性能。但是,由于分析器会禁用JIT问题,所以使用分析器也很困难。 - Reed Copsey

0

我认为64位JIT没有完全开发/移植以充分利用这种64位体系结构的CPU,因此它存在问题,您可能会得到您的程序集的“模拟”行为,这可能会导致问题和意外行为。 我建议您尽量避免这种情况,并且或许可以查找一个快速而好的64位C++编译器来编写时间关键的计算和算法。但即使您在查找信息时遇到困难或没有时间阅读反汇编代码,我相信将重计算放在托管代码之外将减少您可能遇到的任何问题并提高性能 [我相当肯定您已经在这样做了,但只是想提一下:]


0

分析器不应明显影响您的计时结果。如果分析器的开销确实“很大”,那么您可能无法从代码中挤出更多速度,而应该考虑查看硬件瓶颈(磁盘、RAM 或 CPU?)并进行升级。(听起来您是 CPU 受限,所以从这里开始)

通常情况下,.net 和 JIT 使您免于大部分 64 位移植问题。正如您所知,有关寄存器大小(内存使用量变化、编组到本机代码、需要所有程序部件为本机 64 位构建)以及某些性能差异(更大的内存映射、更多寄存器、更宽总线等)的影响,因此我无法告诉您比您已经知道的更多。我看到的其他问题是操作系统问题,而不是 C# 问题 - 现在为 64 位和 WOW64 应用程序提供了不同的注册表 hive,例如,因此有些注册表访问必须小心编写。

通常来说,担心JIT会对你的代码做出什么调整并试图使其更好地工作是一个不好的想法,因为JIT很可能会随着.net 4或5或6而改变,你的“优化”可能会变成低效率,甚至更糟的是,出现错误。此外,请记住,JIT专门为它运行的CPU编译代码,因此在开发PC上的改进可能在其他PC上并没有改进。今天使用的JIT在今天的CPU上可以得到的结果,在一年后升级某些东西时可能会让你感到困扰。
具体来说,您引用了“x64上的属性不会内联”。当您运行完整个代码库并将所有属性转换为字段时,可能会有一个新的64位JIT可以内联属性。实际上,它可能比您的“解决方法”代码表现更好。让微软为您进行优化。
您正确指出了您的内存配置文件可能会发生变化。因此,您可能需要更多的RAM、更快的虚拟内存磁盘和更大的CPU缓存。这些都是硬件问题。您可以通过使用(例如)Int32而不是int来减少影响,但这可能没有太大的区别,并且可能会损害性能(因为您的CPU可能比半尺寸32位值更有效地处理本机64位值)。

你说“启动时间可能会更长”,但在一个你说要运行数小时且CPU占用率为100%的应用程序中,这似乎不太相关。

那么你真正担心什么?也许可以在32位PC上计时你的代码,然后在64位PC上执行相同的任务并计时。在4小时的运行中是否有半小时的差异?还是只有3秒钟的差异?或者64位PC实际上更快?也许你正在寻找不存在的问题的解决方案。

因此,回到通常更通用的建议。进行分析和计时以识别瓶颈。查看您正在应用的算法和数学过程,并尝试使用更高效的算法/替换它们。检查您的多线程方法是否有助于而不是损害性能(即避免等待和锁定)。尝试减少内存分配/释放-例如重复使用对象而不是用新对象替换它们。尝试减少频繁函数调用和虚函数的使用。切换到C++并摆脱垃圾收集、边界检查等.net所施加的固有开销。嗯。这些都与64位无关,对吧?


-1

我对64位问题不是很熟悉,但我有一个评论:

我们应该忘记小的效率问题,大约97%的时间:过早地优化是万恶之源。 -- 唐纳德·克努斯


正如我所说,我的应用程序非常受CPU限制。我有运行时间长达5小时的进程。你评论的反面是,有3%的时间它不是万恶之源。以Rico Mariani的评论为例-如果只有3%的时间很重要,那么意味着33行代码中有一行是关于优化的。 - Reed Copsey
出于好奇,如果您在VS中针对64位平台而不是默认的任何CPU进行目标设置,这些问题是否仍然存在? - Powerlord
是的。它们都是核心平台的问题。64位CLR的JIT与32位JIT完全是不同的代码库,因此它们的性能表现也非常不同。 - Reed Copsey

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