真正的不安全代码性能
Posted
技术标签:
【中文标题】真正的不安全代码性能【英文标题】:True Unsafe Code Performance 【发布时间】:2011-07-19 11:46:04 【问题描述】:我了解不安全代码更适合访问 Windows API 之类的东西并执行不安全类型转换,而不是编写性能更高的代码,但我想问一下您是否注意到实际应用程序的任何显着性能改进与安全的 c# 代码相比,使用它。
【问题讨论】:
P/Invoke 与unsafe
不太一样...我不确定推理是否如下...此外:您是否测量过您是否在这里做一些有用的事情?
无论是安全还是不安全,实际上都不应该具有更高的性能。总体性能取决于您在代码中实现的算法。
我现在没有使用不安全的代码。我只是想了解是否值得将代码的关键部分更改为不安全的代码。
您为什么认为“不安全”意味着“更好的性能”?这只适用于几个专门的场景。
在考虑 unsafe
之前,请确保您知道如何有效地使用 C#。避免过度创建临时对象。何时使用结构数组,以及需要注意的问题。 Buffer.BlockCopy
。 JIT 优化数组边界检查的条件。 (我不是专家,只是说说想到的。)Google C# 高性能 和 C# 性能提示。
【参考方案1】:
一些性能测量
性能优势并不像您想象的那么大。
我对 C# 中的正常托管数组访问与不安全指针进行了一些性能测量。
在 Visual Studio 2010、.NET 4 之外运行的构建结果,使用 任何 CPU |发布版本基于以下 PC 规范:基于 x64 的 PC,1 个四核处理器。 Intel64 Family 6 Model 23 Stepping 10 GenuineIntel ~2833 Mhz.
Linear array access 00:00:07.1053664 for Normal 00:00:07.1197401 for Unsafe *(p + i) Linear array access - with pointer increment 00:00:07.1174493 for Normal 00:00:10.0015947 for Unsafe (*p++) Random array access 00:00:42.5559436 for Normal 00:00:40.5632554 for Unsafe Random array access using Parallel.For(), with 4 processors 00:00:10.6896303 for Normal 00:00:10.1858376 for Unsafe
请注意,不安全的*(p++)
习惯用法实际上运行得更慢。我猜这破坏了编译器优化,它在安全版本中结合了循环变量和(编译器生成的)指针访问。
github 上提供源代码。
【讨论】:
-1。当时已经是一个很好的例子,如何不衡量性能,因为基本上试用代码太简单了。 太琐碎了。它没有考虑编译器所做/可以做的优化。因此,不确定这些数字是否有任何意义。 当然,关键是要衡量改变习语是否允许编译器进行优化——例如,通过删除边界检查? 很好的答案,但也许你可以在看过这个之后改进你的代码。我认为值得重新运行您的实验:referencesource.microsoft.com/#mscorlib/system/text/… @martijnn2008 评论中的文本://读取下一个字符。 // 编译“ch = *pSrc++;”时,JIT 优化似乎变得混乱,所以宁愿使用“ch = *pSrc; pSrc++;”而是 ch = *pSrc; pSrc++;【参考方案2】:正如在其他帖子中所述,您可以在非常专业的上下文中使用不安全的代码来获得显着的性能改进。其中一种情况是迭代值类型的数组。使用不安全的指针算法比使用通常的 for-loop/indexer 模式要快得多..
struct Foo
int a = 1;
int b = 2;
int c = 0;
Foo[] fooArray = new Foo[100000];
fixed (Foo* foo = fooArray) // foo now points to the first element in the array...
var remaining = fooArray.length;
while (remaining-- > 0)
foo->c = foo->a + foo->b;
foo++; // foo now points to the next element in the array...
这里的主要好处是我们完全取消了数组索引检查..
虽然非常高效,但这种代码很难处理,可能非常危险(不安全),并且违反了一些基本准则(可变结构)。但肯定有适合的场景......
【讨论】:
这种代码的另一个大缺点是没有很大比例的 C# 程序员理解它或它的含义。如果您在团队中工作,请采用 KISS 原则... 你确定这个特定的例子要快得多吗? Sun 的 JVM 设法使这种类型的代码(在 Java 或 Scala 中)几乎与使用指针算法的 C++ 一样快;我很惊讶 C# 实现不会做同样的事情。 这个特定的例子,不,因为编译器可以确定 i 永远不会超出数组的边界,从而跳过数组边界检查。但原则仍然存在。在我的特殊情况下,我有一个使用数组实现的环形缓冲区,以及一个单独的迭代器对象来迭代它。在这种情况下,编译器无法进行此优化.. 同意环形缓冲区,至少对于大小是 2 的整数倍。如果不是两个,编译器(真实的或即时的)可以想象你的测试总是固定范围,因此它不需要再次重复完全相同的测试。但是,我不知道这是否完成;最近不需要高性能环形缓冲区。 在我的测试中使用固定和指针实际上更慢(Win 7,x64)。不要假设任何事情,您需要衡量。【参考方案3】:图像处理就是一个很好的例子。通过使用指向其字节的指针(这需要不安全的代码)来修改像素要快得多。
示例:http://www.gutgames.com/post/Using-Unsafe-Code-for-Faster-Image-Manipulation.aspx
话虽如此,对于大多数情况,差异不会那么明显。因此,在您使用不安全代码之前,请分析您的应用程序以查看性能瓶颈在哪里,并测试不安全代码是否真的是提高速度的解决方案。
【讨论】:
安全图像处理也可以很快。特别是如果您的算法可以编写为消除边界检查。我只使用不安全的代码将数据从位图中复制到数组中并返回。如果您使用byte[]
,您甚至可以避免这种情况,而只需使用Marshal
函数。
@Botz3000:这是一个很棒的链接(期待阅读该网站的其余部分),但结论并不可靠。使用GetPixel
和SetPixel
确实很慢,但使用int[]
或多或少与使用指针一样快,但没有缺点。
@CodeInChaos:我必须同意。我得出的结论是,唯一受益的领域是复制位图数据。不过还没有尝试过使用Marshal
。
我看到那篇文章中的代码有很多问题。在内部循环中迭代 y 意味着内存不是按顺序访问的。此外,从 C# 属性访问诸如高度之类的基元,而不是首先将该基元复制到局部变量中,这意味着正在发生大量昂贵的堆提取操作。第三,在循环中使用对 GetPixel 的函数调用而不是内联指针算术涉及大量开销。您正在使用该文章中的代码查看大约 100 倍的性能损失。这是一个如何使用不安全代码的可怕例子。
@Botz3000 就像我上面所说的,您看到的性能损失大约是 100 倍(基于我自己完成的测试)。当然它会比使用 Graphics.GetPixel 更快,但我的 I7 也比我在车库里的 Commodore 更快。一篇关于使用不安全代码进行更快计算的文章不应该在 IMO 中出现太多错误。【参考方案4】:
好吧,我建议阅读这篇博文:MSDN blogs: Array Bounds Check Elimination in the CLR
这阐明了如何在 C# 中完成边界检查。此外,Thomas Bratts 测试在我看来毫无用处(查看代码),因为 JIT 在他的“保存”循环中删除了边界检查。
【讨论】:
你能在这里总结一下这篇文章吗?如果链接变暗,您的答案将不会很有帮助。 你错过了测试的重点——它们不是为了展示边界检查与不安全的效果。他们将表明边界检查通常被优化掉,并且在这些情况下不安全的成本可能不值得。【参考方案5】:我正在为视频操作代码使用不安全的代码。 在这样的代码中,您希望它尽可能快地运行,而无需对值等进行内部检查。如果没有不安全的属性,我将无法跟上 30 fps 或 60 fps 的视频流。 (取决于使用的相机)。
但由于速度,它被图形编码人员广泛使用。
【讨论】:
【参考方案6】:对于所有正在查看这些答案的人,我想指出,尽管答案非常好,但已经发布了很多问题。
请注意,.net 已经发生了很大变化,现在还可以访问新的数据类型,如向量、Span、ReadOnlySpan 以及硬件特定的库和类,如核心 3.0 中的System.Runtime.Intrinsics 中的那些
查看此blog 帖子以了解如何使用硬件优化循环,以及此blog how to fallback 以了解在最佳硬件不可用时的安全方法。
【讨论】:
以上是关于真正的不安全代码性能的主要内容,如果未能解决你的问题,请参考以下文章
即使在 project.json 中将 allowunsafe 标志设置为 true 后,.NET Core 中的不安全代码编译错误