在C#中,使用UInt32和Int32是否会有显著的性能差异?

17

我正在将一个现有应用程序移植到C#,并希望尽可能提高性能。许多现有的循环计数器和数组引用是定义为System.UInt32,而不是我本来会使用的Int32。

使用UInt32与Int32相比是否存在显着的性能差异?

8个回答

35
简短的回答是“不会有任何性能影响”。
正确的回答是“这取决于情况”。
更好的问题是,“当我确定不需要符号时,我应该使用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;
  }
}

两种测试方法,TestSignedTestUnsigned,分别对有符号整数和无符号整数进行了约200万次简单的递增操作。测试代码运行每个测试100次并对结果取平均值。这应该能够消除任何潜在的不一致性。在我的i7-5960X编译为x64的计算机上,结果如下:
Signed:      00:00:00.5066966

Unsigned:    00:00:00.5052279

这些结果几乎相同,但为了得出确定的答案,我们真正需要查看程序生成的字节码。我们可以使用.NET SDK中的ILDASM来检查编译器生成的程序集中的代码。

Bytecode

在这里,我们可以看到C#编译器偏爱有符号整数,并且实际上大部分操作都是以有符号整数原生执行的,只有在比较分支(又称跳转或if)时才将值在内存中视为无符号。尽管在TestUnsigned方法中,我们对迭代器和累加器都使用了无符号整数,但代码与TestSigned方法几乎相同,除了一个指令:IL_0016。快速查看ECMA规范描述了它们之间的区别:

blt.un.s : 短格式,如果小于(无符号或无序),则跳转至目标。

blt.s : 短格式,如果小于,则跳转至目标。

作为一条常见的指令,可以安全地假定大多数现代高功率处理器都将拥有操作的硬件指令,并且它们很可能在相同数量的周期内执行,但这并不是保证。低功率处理器可能具有较少的指令,并且没有无符号整数的分支。在这种情况下,JIT编译器可能需要发出多个硬件指令(例如首先进行转换,然后进行分支)来执行blt.un.s IL指令。即使是这种情况,这些附加指令也将是基本的,可能不会对性能产生显著影响。
因此,在性能方面,长答案是“使用带符号或无符号整数之间几乎不可能存在性能差异。如果有差异,那么可能是微不足道的。”
那么,如果性能相同,下一个合乎逻辑的问题是,“当我确定不需要符号时,应该使用无符号值吗?”

这里有两件事情需要考虑:首先,无符号整数不符合CLS-compliant标准,这意味着如果您将无符号整数作为API的一部分暴露给另一个程序(例如如果您正在分发可重用库),则可能会遇到问题。其次,.NET中的大多数操作(包括BCL公开的方法签名)都使用带符号整数(出于上述原因)。因此,如果您计划实际使用无符号整数,则可能会发现自己经常进行强制转换。这会对性能造成非常小的影响,并使您的代码有点混乱。最终,这可能并不值得。

TLDR; 回顾我在C ++时代的话,“使用最恰当的东西,让编译器解决其余的问题。” C#并不是那么简单明了,所以我会说这个建议适用于.NET:在x86 / x64上,带符号和无符号整数之间实际上没有性能差异,但大多数操作需要带符号整数,因此除非您真正需要限制仅为正值,或者您确实需要符号位占用的额外范围,否则请坚持使用带符号整数。最终,您的代码将更加简洁。


3
我一直想知道,为什么有人想要查看某些高级语言代码生成的字节码?现在我明白了。 - wodzu

12

我认为没有什么性能上的考虑,除了处理器级别上可能存在的有符号和无符号算术差异,但此时我认为这些差异是无关紧要的。

更大的差异在于CLS兼容性,因为某些语言不支持无符号类型,所以无符号类型不符合CLS标准。


3

我没有在.NET方面进行任何研究,但在Win32/C++的旧日子里,如果您想将“signed int”转换为“signed long”,CPU必须运行一个操作来扩展符号。要将“unsigned int”转换为“unsigned long”,只需在上位字节中填充零即可。这样可以节省几个时钟周期(即,您必须执行数十亿次才能产生明显的差异)。


3

就性能而言,没有区别。简单的整数计算是众所周知的,现代CPU已经高度优化以快速执行它们。


1

这些类型的优化很少值得努力。使用最适合任务的数据类型,然后就不要再管它了。如果这个东西碰到数据库,你可能会发现在数据库设计、查询语法或索引策略方面进行一打调整就能够抵消在 C# 代码优化方面所取得的成果。


7
“a few hundreds of orders of magnitude”至少是10^200,这比可见宇宙中的原子数量还要多得多。我认为你有些过分了。 - kaalus

0

无论哪种方式,它都将分配相同数量的内存(尽管其中一种可以存储更大的值,因为它不会为符号保存空间)。因此,除非您使用会导致其中一种选项爆炸的大值/负值,否则我怀疑您是否会看到“性能”差异。


它们都存储相同的范围,只是有符号整数的一半范围是负数。 - GalacticCowboy
2
更准确地说,它们存储相同数量的值,因为它们的范围明显不同。 - Hosam Aly

0
我从未理解在循环中使用int的原因,例如for(int i=0;i<bla;i++)。而且,我经常想使用unsigned来避免检查范围。不幸的是(无论是在C++还是在C#中出于类似的原因),建议不要使用unsigned来获得一个比特或确保非负性,如下所述:

"使用unsigned代替int来获得一个比特以表示正整数几乎从来不是一个好主意。通过声明变量unsigned来确保某些值为正数的尝试通常会被隐式转换规则所打败"

这段话摘自“C++程序设计语言”第73页,作者是该语言的创造者Bjarne Stroustrup。
我的理解是(抱歉手头没有来源),硬件制造商也有优化integer类型的偏见。
尽管如此,使用带有一些正性assertintegerunsigned进行相同的练习将是有趣的。

0

这与性能无关,而是与循环计数器的要求有关。

也许需要完成许多迭代。

        Console.WriteLine(Int32.MaxValue);      // Max interation 2147483647
        Console.WriteLine(UInt32.MaxValue);     // Max interation 4294967295

无符号整数可能有其存在的意义。

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