我正在将现有应用程序移植到 C# 并希望尽可能提高性能。许多现有的循环计数器和数组引用被定义为 System.UInt32,而不是我会使用的 Int32。
使用 UInt32 和 Int32 有什么显着的性能差异吗?
我正在将现有应用程序移植到 C# 并希望尽可能提高性能。许多现有的循环计数器和数组引用被定义为 System.UInt32,而不是我会使用的 Int32。
使用 UInt32 和 Int32 有什么显着的性能差异吗?
简短的回答是“不。任何性能影响都可以忽略不计”。
正确答案是“视情况而定”。
一个更好的问题是,“当我确定不需要符号时,我应该使用 uint 吗?”
您无法就性能给出明确的“是”或“否”的原因是目标平台最终将决定性能。也就是说,性能取决于将要执行代码的任何处理器以及可用的指令。您的 .NET 代码编译为中间语言(IL 或字节码)。这些指令随后由即时(JIT) 编译器编译到目标平台,作为公共语言运行时(CLR) 的一部分。您无法控制或预测将为每个用户生成什么代码。
因此,知道硬件是性能的最终仲裁者,问题就变成了“.NET 为有符号整数和无符号整数生成的代码有何不同?” 和“差异会影响我的应用程序和目标平台吗?”
回答这些问题的最佳方法是进行测试。
class Program
{
static void Main(string[] args)
{
const int iterations = 100;
Console.WriteLine($"Signed: {Iterate(TestSigned, iterations)}");
Console.WriteLine($"Unsigned: {Iterate(TestUnsigned, iterations)}");
Console.Read();
}
private static void TestUnsigned()
{
uint accumulator = 0;
var max = (uint)Int32.MaxValue;
for (uint i = 0; i < max; i++) ++accumulator;
}
static void TestSigned()
{
int accumulator = 0;
var max = Int32.MaxValue;
for (int i = 0; i < max; i++) ++accumulator;
}
static TimeSpan Iterate(Action action, int count)
{
var elapsed = TimeSpan.Zero;
for (int i = 0; i < count; i++)
elapsed += Time(action);
return new TimeSpan(elapsed.Ticks / count);
}
static TimeSpan Time(Action action)
{
var sw = new Stopwatch();
sw.Start();
action();
sw.Stop();
return sw.Elapsed;
}
}
两种测试方法TestSigned和TestUnsigned分别对有符号和无符号整数执行约 200 万次简单增量迭代。测试代码对每个测试运行 100 次迭代并对结果进行平均。这应该消除任何潜在的不一致。我为 x64 编译的 i7-5960X 上的结果是:
Signed: 00:00:00.5066966
Unsigned: 00:00:00.5052279
这些结果几乎相同,但要获得明确的答案,我们确实需要查看为程序生成的字节码。我们可以使用ILDASM作为 .NET SDK 的一部分来检查编译器生成的程序集中的代码。
在这里,我们可以看到 C# 编译器偏爱有符号整数,并且实际上在本机作为有符号整数执行大多数操作,并且仅在比较分支(也称为跳转或 if)时将内存中的值视为无符号。尽管我们在TestUnsigned中对迭代器和累加器都使用了无符号整数,但代码几乎与TestSigned方法相同,除了一条指令:IL_0016。快速浏览ECMA 规范描述了不同之处:
blt.un.s :如果小于(无符号或无序),则分支到目标,短格式。
blt.s :如果小于,则分支到目标,短格式。
作为如此常见的指令,可以安全地假设大多数现代高功率处理器将具有用于这两种操作的硬件指令,并且它们很可能会在相同数量的周期内执行,但这并不能保证。低功耗处理器可能有更少的指令,并且没有 unsigned int 的分支。在这种情况下,JIT 编译器可能必须发出多个硬件指令(例如,首先是转换,然后是分支)来执行blt.un.s IL 指令。即使是这种情况,这些附加指令也是基本的,可能不会显着影响性能。
因此,就性能而言,长答案是“使用有符号整数或无符号整数之间不太可能存在性能差异。如果存在差异,则可能可以忽略不计。”
因此,如果性能相同,下一个合乎逻辑的问题是,“当我确定不需要符号时,我应该使用无符号值吗?”
这里有两件事要考虑:首先,无符号整数不符合 CLS,这意味着如果您将无符号整数作为另一个程序将使用的 API 的一部分公开(例如,如果您是分发一个可重用的库)。其次,.NET 中的大多数操作,包括 BCL 公开的方法签名(出于上述原因),都使用有符号整数。因此,如果您打算实际使用无符号整数,您可能会发现自己对它进行了相当多的转换。这将对性能造成非常小的影响,并且会使您的代码更加混乱。最后,它可能不值得。
TLDR;回到我的 C++ 时代,我会说“使用最合适的东西,让编译器解决剩下的问题。” C# 并不那么简单,所以我会为 .NET 这么说:x86/x64 上的有符号整数和无符号整数之间确实没有性能差异,但大多数操作都需要有符号整数,所以除非你真的需要仅将值限制为正数,或者您确实需要符号位所吃的额外范围,请坚持使用有符号整数。你的代码最终会更干净。
除了处理器级别的有符号和无符号算术之间可能存在的差异之外,我认为没有任何性能考虑,但在这一点上,我认为这些差异是没有实际意义的。
更大的区别在于 CLS 合规性,因为无符号类型不符合 CLS,因为并非所有语言都支持它们。
我没有在 .NET 中对此事进行任何研究,但是在 Win32/C++ 的旧时代,如果你想将“signed int”转换为“signed long”,cpu 必须运行一个 op 来扩展标志。要将“unsigned int”转换为“unsigned long”,它的高字节只有零。节省大约几个时钟周期(即,您必须执行数十亿次才能获得甚至可察觉的差异)
没有区别,性能方面。简单的整数计算是众所周知的,现代 CPU 经过高度优化以快速执行它们。
这些类型的优化很少值得付出努力。使用最适合该任务的数据类型并保留它。如果这件事涉及到数据库,您可能会在数据库设计、查询语法或索引策略中找到十几个调整,这些调整将使 C# 中的代码优化抵消几百个数量级。
无论哪种方式,它都会分配相同数量的内存(尽管一个可以存储更大的值,因为它不会为符号节省空间)。因此,我怀疑您会看到“性能”差异,除非您使用会导致一个选项或另一个选项爆炸的大值/负值。
这实际上与性能无关,而是与循环计数器的要求有关。
Prehaps有很多迭代要完成
Console.WriteLine(Int32.MaxValue); // Max interation 2147483647
Console.WriteLine(UInt32.MaxValue); // Max interation 4294967295
unsigned int 可能是有原因的。
我从来没有同情int
in loops的使用for(int i=0;i<bla;i++)
。通常我也想用unsigned
它来避免检查范围。不幸的是(在 C++ 和 C# 中出于类似原因),建议不要使用unsigned
来获得更多位或确保非消极性,:
“使用 an
unsigned
而不是 anint
来获得更多位来表示正整数几乎从来都不是一个好主意。通过声明变量来确保某些值是正数的尝试unsigned
通常会被隐式转换规则打败”
该语言的创建者Bjarne Stroustrup的“C++ 编程语言”的第 73 页。
我的理解(我很抱歉没有手头的来源)是硬件制造商也有优化integer
类型的偏见。
尽管如此,做上面#Robear所做的相同练习会很有趣,但使用integer
一些积极性assert
与unsigned
.