【问题标题】:int, short, byte performance in back-to-back for-loops背靠背for循环中的int,short,byte性能
【发布时间】:2011-02-05 08:17:54
【问题描述】:

(背景:Why should I use int instead of a byte or short in C#

为了满足我自己对使用“适当大小”整数与“优化”整数的优缺点的好奇心,我编写了以下代码,它强化了我之前对 .Net 中 int 性能的看法(并对此进行了解释)在上面的链接中),即它针对 int 性能而不是 short 或 byte 进行了优化。

DateTime t;
long a, b, c;

t = DateTime.Now;
for (int index = 0; index < 127; index++)
{
    Console.WriteLine(index.ToString());
}           
a = DateTime.Now.Ticks - t.Ticks;

t = DateTime.Now;
for (short index = 0; index < 127; index++)
{
    Console.WriteLine(index.ToString());
}
        
b=DateTime.Now.Ticks - t.Ticks;

t = DateTime.Now;           
for (byte index = 0; index < 127; index++)
{
    Console.WriteLine(index.ToString());
}
c=DateTime.Now.Ticks - t.Ticks;

Console.WriteLine(a.ToString());
Console.WriteLine(b.ToString());
Console.WriteLine(c.ToString());

这在...方面给出了大致一致的结果

~950000

~2000000

~1700000

这符合我的预期。

但是,当我尝试像这样为每种数据类型重复循环时...

t = DateTime.Now;
for (int index = 0; index < 127; index++)
{
    Console.WriteLine(index.ToString());
}
for (int index = 0; index < 127; index++)
{
    Console.WriteLine(index.ToString());
}
for (int index = 0; index < 127; index++)
{
    Console.WriteLine(index.ToString());
}
a = DateTime.Now.Ticks - t.Ticks;

数字更像...

~4500000

~3100000

~300000

这让我感到困惑。谁能解释一下?

注意: 由于 byte 值类型的范围,为了比较like for like,我将循环限制为127。 这也是好奇心的行为,而不是生产代码的微优化。

【问题讨论】:

  • byte 的范围为 0-255。它不是带符号的数据类型。
  • 另外,DateTime 类不适合低级性能分析。使用System.Diagnostics.Stopwatch
  • @Aaronaught,乔恩:感谢您的解决方案。我有一些澄清 ...index

标签: c# performance types for-loop


【解决方案1】:

首先,不是 .NET 针对 int 性能进行了优化,而是 机器 进行了优化,因为 32 位是本机字长(除非您使用的是 x64,在这种情况下它是long 或 64 位)。

其次,您要在每个循环内写入控制台 - 这比递增和测试循环计数器要昂贵得多,因此您在这里没有测量任何实际的东西。

第三,byte 的范围最大为 255,因此您可以循环 254 次(如果您尝试执行 255 它将溢出并且循环将永远不会结束 - 但您不需要在 128 处停止)。

第四,您没有在任何地方接近进行足够的迭代来分析。迭代一个紧密的循环 128 甚至 254 次是没有意义的。您应该做的是将byte/short/int 循环放在另一个循环中,该循环迭代次数要多得多,比如 1000 万次,然后检查结果。

最后,在计算中使用DateTime.Now 会在分析时产生一些计时“噪音”。建议(并且更容易)改用 Stopwatch 类。

归根结底,这需要 许多 更改才能成为有效的性能测试。


这是我认为更准确的测试程序:

class Program
{
    const int TestIterations = 5000000;

    static void Main(string[] args)
    {
        RunTest("Byte Loop", TestByteLoop, TestIterations);
        RunTest("Short Loop", TestShortLoop, TestIterations);
        RunTest("Int Loop", TestIntLoop, TestIterations);
        Console.ReadLine();
    }

    static void RunTest(string testName, Action action, int iterations)
    {
        Stopwatch sw = new Stopwatch();
        sw.Start();
        for (int i = 0; i < iterations; i++)
        {
            action();
        }
        sw.Stop();
        Console.WriteLine("{0}: Elapsed Time = {1}", testName, sw.Elapsed);
    }

    static void TestByteLoop()
    {
        int x = 0;
        for (byte b = 0; b < 255; b++)
            ++x;
    }

    static void TestShortLoop()
    {
        int x = 0;
        for (short s = 0; s < 255; s++)
            ++x;
    }

    static void TestIntLoop()
    {
        int x = 0;
        for (int i = 0; i < 255; i++)
            ++x;
    }
}

这会在一个更大的循环(500 万次迭代)内运行每个循环,并在循环内执行一个非常简单的操作(增加一个变量)。我的结果是:

字节循环:经过的时间 = 00:00:03.8949910
短循环:经过的时间 = 00:00:03.9098782
内部循环:经过的时间 = 00:00:03.2986990

所以,没有明显区别。

另外,请确保您在发布模式下进行分析,很多人会忘记并在调试模式下进行测试,这会大大降低准确度。

【讨论】:

  • 哦,谢谢,我以前从未真正尝试过分析我的代码。好点,在船上:)
  • @Jon:我发誓我没有抄袭你的。 :P
  • 哦,我根本没想到。只是被逗乐了。
  • 将答案分开并不是什么大问题,因此大众投票将其拿走。干杯,伙计们。
  • >"第三,一个字节的范围最大为 255,所以你可以循环 254 次"这是我觉得麻烦的事情;如果您使用由字节支持的 for 循环,实际上不可能让它遍历每个可能的字节值,并且您需要使用更大的数据类型?这看起来很愚蠢,但我明白为什么会这样。
【解决方案2】:

大部分时间可能都花在了写控制台上。尝试在循环中做其他事情......

另外:

  • 使用DateTime.Now 是一种不好的时间测量方式。请改用System.Diagnostics.Stopwatch
  • 一旦您摆脱了Console.WriteLine 调用,127 次迭代的循环将太短而无法测量。您需要运行循环 很多次 次才能获得合理的测量结果。

这是我的基准:

using System;
using System.Diagnostics;

public static class Test
{    
    const int Iterations = 100000;

    static void Main(string[] args)
    {
        Measure(ByteLoop);
        Measure(ShortLoop);
        Measure(IntLoop);
        Measure(BackToBack);
        Measure(DelegateOverhead);
    }

    static void Measure(Action action)
    {
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();
        Stopwatch sw = Stopwatch.StartNew();
        for (int i = 0; i < Iterations; i++)
        {
            action();
        }
        sw.Stop();
        Console.WriteLine("{0}: {1}ms", action.Method.Name,
                          sw.ElapsedMilliseconds);
    }

    static void ByteLoop()
    {
        for (byte index = 0; index < 127; index++)
        {
            index.ToString();
        }
    }

    static void ShortLoop()
    {
        for (short index = 0; index < 127; index++)
        {
            index.ToString();
        }
    }

    static void IntLoop()
    {
        for (int index = 0; index < 127; index++)
        {
            index.ToString();
        }
    }

    static void BackToBack()
    {
        for (byte index = 0; index < 127; index++)
        {
            index.ToString();
        }
        for (short index = 0; index < 127; index++)
        {
            index.ToString();
        }
        for (int index = 0; index < 127; index++)
        {
            index.ToString();
        }
    }

    static void DelegateOverhead()
    {
        // Nothing. Let's see how much
        // overhead there is just for calling
        // this repeatedly...
    }
}

结果:

ByteLoop: 6585ms
ShortLoop: 6342ms
IntLoop: 6404ms
BackToBack: 19757ms
DelegateOverhead: 1ms

(这是在上网本上 - 调整迭代次数,直到你得到合理的结果:)

这似乎表明它与您使用的类型基本没有显着差异。

【讨论】:

  • 但所有循环都写入控制台的次数相同,即 127 x n-loops
  • 虽然我猜 int.toString() 可能需要比 byte.toString() 更长的时间?
  • @runrunraygun: Console.WriteLine 是一个执行时间不可靠的异步操作。虽然它不太可能对您的结果产生巨大影响,但请使用更可靠的东西。此外,int.ToString()byte.ToString() 的功能不同,因此您不会在每个循环中执行相同的操作。
  • @Adam:我在我的基准测试中保留了 int.ToString 与 byte.ToString() 的区别,但删除了 Console.WriteLine 调用。所以这是测试“用int循环并将int转换为字符串”和“用short循环并将short转换为字符串”等。
  • 我无法想象它实际上很重要,但实际上没有必要处理测量循环中的循环变量,是吗?您可以在循环内调用 ToString() 的每个函数中轻松地拥有一个 int 变量。
【解决方案3】:

出于好奇,我从 Aaronaught 修改了一点程序,并在 x86 和 x64 模式下编译它。奇怪的是,Int 在 x64 中工作得更快:

x86

字节循环:经过的时间 = 00:00:00.8636454
短循环:经过时间 = 00:00:00.8795518
UShort 循环:经过的时间 = 00:00:00.8630357
内部循环:经过的时间 = 00:00:00.5184154
UInt 循环:经过的时间 = 00:00:00.4950156
长循环:经过的时间 = 00:00:01.2941183
超长循环:经过的时间 = 00:00:01.3023409

x64

字节循环:经过的时间 = 00:00:01.0646588
短循环:经过时间 = 00:00:01.0719330
UShort 循环:经过的时间 = 00:00:01.0711545
内部循环:经过时间 = 00:00:00.2462848
UInt 循环:经过的时间 = 00:00:00.4708777
长循环:经过的时间 = 00:00:00.5242272
超长循环:经过的时间 = 00:00:00.5144035

【讨论】:

    【解决方案4】:

    我尝试了上面的两个程序,因为它们看起来会在我的开发机器上产生不同且可能相互冲突的结果。

    Aaronaughts 测试工具的输出

    Short Loop: Elapsed Time = 00:00:00.8299340
    Byte Loop: Elapsed Time = 00:00:00.8398556
    Int Loop: Elapsed Time = 00:00:00.3217386
    Long Loop: Elapsed Time = 00:00:00.7816368
    

    int 更快

    乔恩的输出

    ByteLoop: 1126ms
    ShortLoop: 1115ms
    IntLoop: 1096ms
    BackToBack: 3283ms
    DelegateOverhead: 0ms
    

    什么都没有

    Jon 在结果中调用 tostring 有一个很大的固定常数,这可能隐藏了如果在循环中完成的工作较少可能出现的好处。 Aaronaught 使用的是 32 位操作系统,使用 int 似乎并没有像我使用的 x64 装备那样受益。

    硬件/软件 结果是在 3.33GHz 的 Core i7 975 上收集的,涡轮已禁用,并且设置了核心亲和性以减少其他任务的影响。性能设置全部设置为最大值,病毒扫描程序/不必要的后台任务暂停。 Windows 7 x64 终极版,具有 11 GB 的备用内存和很少的 IO 活动。在内置于 vs 2008 的发布配置中运行,无需附加调试器或分析器。

    可重复性 最初为每个测试重复 10 次更改执行顺序。变化可以忽略不计,所以我只发布了我的第一个结果。在最大 CPU 负载下,执行时间的比率保持一致。在考虑 CPU 生成和 Ghz 后,在多个 x64 xp xeon 刀片上重复运行得到大致相同的结果

    分析 Redgate / Jetbrains / Slimtune / CLR profiler 和我自己的 profiler 都表明结果是正确的。

    调试构建 使用 VS 中的调试设置可以得到与 Aaronaught 一致的结果。

    【讨论】:

    • 我正在运行一个 x64 机器。对于第一次测试来说,这是一个非常反常的结果——看起来shortbyte 版本花费的时间比他们应该的要长得多,而int 版本非常接近我的。你测试了几次?你有没有同时运行其他东西?
    • 您是否尝试过重新排序短字节整数循环以查看是否有任何区别?以防万一 JIT 编译器决定第三个循环可能值得优化,因为它似乎是一个常见的操作。只是一个想法。会很有趣。
    • @Aaronaught 将我的配置切换到 x86 dll 使我的结果更加平衡。这就是为什么我认为您使用的是 32 位操作系统。
    • @Skizz 我早早地排除了多次乱序运行。查看对我的帖子的修改
    【解决方案5】:

    游戏有点晚了,但这个问题值得一个准确的答案。

    int 循环生成的IL 代码确实会比其他两个循环更快。当使用byteshort 时,需要转换指令。但是,可能在某些条件下(不在本分析范围内)抖动能够将其优化掉。

    基准测试

    使用Release (Any CPU) 配置定位.NET Core 3.1。在x64 CPU 上执行基准测试。

    
    |    Method |      Mean |    Error |   StdDev |
    |---------- |----------:|---------:|---------:|
    |  ByteLoop | 149.78 ns | 0.963 ns | 0.901 ns |
    | ShortLoop | 149.40 ns | 0.322 ns | 0.286 ns |
    |   IntLoop |  79.38 ns | 0.764 ns | 0.638 ns |
    

    生成的 IL

    比较三种方法的IL,很明显诱导成本来自conv指令。

    IL_0000:  ldc.i4.0
    IL_0001:  stloc.0
    IL_0002:  br.s       IL_0009
    IL_0004:  ldloc.0
    IL_0005:  ldc.i4.1
    IL_0006:  add
    IL_0007:  conv.i2   ; conv.i2 for short, conv.i4 for byte
    IL_0008:  stloc.0
    IL_0009:  ldloc.0
    IL_000a:  ldc.i4     0xff
    IL_000f:  blt.s      IL_0004
    IL_0011:  ret
    

    完整的测试代码

    using BenchmarkDotNet.Attributes;
    using BenchmarkDotNet.Running;
    
    namespace LoopPerformance
    {
        public class Looper
        {
            [Benchmark]
            public void ByteLoop()
            {
                for (byte b = 0; b < 255; b++) {}
            }
    
            [Benchmark]
            public void ShortLoop()
            {
                for (short s = 0; s < 255; s++) {}
            }
    
            [Benchmark]
            public void IntLoop()
            {
                for (int i = 0; i < 255; i++) {}
            }
        }
    
        class Program
        {
            static void Main(string[] args)
            {
                var summary = BenchmarkRunner.Run<Looper>();
            }
        }
    }
    

    【讨论】:

      【解决方案6】:

      分析 .Net 代码非常棘手,因为编译后的字节码在其中运行的运行时环境可以对字节码进行运行时优化。在您的第二个示例中,JIT 编译器可能发现了重复的代码并创建了一个更优化的版本。但是,如果没有对运行时系统如何工作的任何真正详细的描述,就不可能知道您的代码会发生什么。并且基于实验尝试和猜测是愚蠢的,因为微软完全有权随时重新设计 JIT 引擎,只要他们不破坏任何功能。

      【讨论】:

      • 在调试器中运行代码(或者更准确地说,在创建 VS 项目的调试配置文件的默认设置下编译和运行)几乎消除了您进行优化的可能性'正在谈论。
      • @Adam:但谁会在调试器下运行代码。我注意到在 VS2005 中,代码在调试器中的运行速度比单独运行要慢得多。 IIRC,这里有人提到 debug .net 编译器和 release .net 编译器的输出几乎相同,这是代码独立运行而不是在调试器中运行的事实。
      • 禁用优化(默认情况下在调试配置中完成)特别是消除了您所说的那种“优化”。附加 any 调试器可能会对性能产生负面影响,但这是另一个问题。启用优化的编译器的输出确实与禁用优化的输出不同。
      【解决方案7】:

      控制台写入与数据的实际性能无关。它更多地与与控制台库调用的交互有关。建议你在那些与数据大小无关的循环中做一些有趣的事情。

      建议:位移、乘法、数组操作、加法、许多其他...

      【讨论】:

        【解决方案8】:

        除了不同整数数据类型的性能外,我还测试了Int32Int64(即intlong)的性能,以实现我的素数计算器,并发现在我的x64机(锐龙1800X)没有明显区别。

        我无法真正测试短裤(Int16UInt16),因为它很快就会溢出。

        正如其他人所指出的,您的短循环混淆了您的结果,尤其是您的调试语句。您应该尝试改用工作线程。


        这是intlong 的性能对比:

        当然,请确保避免将 long(以及除普通 int 之外的任何内容)用于数组索引,因为您甚至无法使用它们,并且强制转换为 int 只会损害性能(在我的测试)。

        这是我的分析代码,它在工作线程永远旋转时轮询进度。通过重复测试,它确实会稍微变慢,所以我确保在其他顺序和单独测试中也进行测试:

        public static void Run() {
            TestWrapper(new PrimeEnumeratorInt32());
            TestWrapper(new PrimeEnumeratorInt64());
            TestWrapper(new PrimeEnumeratorInt64Indices());
        }
        
        private static void TestWrapper<X>(X enumeration)
        where X : IDisposable, IEnumerator {
            int[] lapTimesMs = new int[] { 100, 300, 600, 1000, 3000, 5000, 10000 };
            int sleepNumberBlockWidth = 2 + (int)Math.Ceiling(Math.Log10(lapTimesMs.Max()));
            string resultStringFmt = string.Format("\tTotal time is {{0,-{0}}}ms, number of computed primes is {{1}}", sleepNumberBlockWidth);
        
            int totalSlept = 0;
            int offset = 0;
            Stopwatch stopwatch = new Stopwatch();
        
            Type t = enumeration.GetType();
            FieldInfo field = t.GetField("_known", BindingFlags.NonPublic | BindingFlags.Instance);
        
            Console.WriteLine("Testing {0}", t.Name);
        
            _continue = true;
            Thread thread = new Thread(InfiniteLooper);
            thread.Start(enumeration);
            stopwatch.Start();
            foreach (int sleepSize in lapTimesMs) {
                SleepExtensions.SleepWithProgress(sleepSize + offset);
        
                //avoid race condition calling the Current property by using reflection to get private data
                Console.WriteLine(resultStringFmt, stopwatch.ElapsedMilliseconds, ((IList)field.GetValue(enumeration)).Count);
        
                totalSlept += sleepSize;
                offset = totalSlept - (int)stopwatch.ElapsedMilliseconds;//synchronize to stopwatch laps
            }
            _continue = false;
            thread.Join(100);//plz stop in time (Thread.Abort is no longer supported)
            enumeration.Dispose();
            stopwatch.Stop();
        }
        
        private static bool _continue = true;
        private static void InfiniteLooper(object data) {
            IEnumerator enumerator = (IEnumerator)data;
            while (_continue && enumerator.MoveNext()) { }
        }
        

        }

        请注意,您可以将 SleepExtensions.SleepWithProgress 替换为 Thread.Sleep

        以及正在分析的算法的三种变体:

        Int32 版本

        class PrimeEnumeratorInt32 : IEnumerator<int> {
            public int Current { get { return this._known[this._currentIdx]; } }
            object IEnumerator.Current { get { return this.Current; } }
        
            private int _currentIdx = -1;
            private List<int> _known = new List<int>() { 2, 3 };
        
            public bool MoveNext() {
                if (++this._currentIdx >= this._known.Count)
                    this._known.Add(this.ComputeNext(this._known[^1]));
                return true;//no end
            }
        
            private int ComputeNext(int lastKnown) {
                int current = lastKnown + 2;//start at 2 past last known value, which is guaranteed odd because we initialize up thru 3
        
                int testIdx;
                int sqrt;
                bool isComposite;
                while (true) {//keep going until a new prime is found
                    testIdx = 1;//all test values are odd, so skip testing the first known prime (two)
                    sqrt = (int)Math.Sqrt(current);//round down, and avoid casting due to the comparison type of the while loop condition
        
                    isComposite = false;
                    while (this._known[testIdx] <= sqrt) {
                        if (current % this._known[testIdx++] == 0L) {
                            isComposite = true;
                            break;
                        }
                    }
        
                    if (isComposite) {
                        current += 2;
                    } else {
                        return current;//and end
                    }
                }
            }
        
            public void Reset() {
                this._currentIdx = -1;
            }
            public void Dispose() {
                this._known = null;
            }
        }
        

        Int64 版本

        class PrimeEnumeratorInt64 : IEnumerator<long> {
            public long Current { get { return this._known[this._currentIdx]; } }
            object IEnumerator.Current { get { return this.Current; } }
        
            private int _currentIdx = -1;
            private List<long> _known = new List<long>() { 2, 3 };
        
            public bool MoveNext() {
                if (++this._currentIdx >= this._known.Count)
                    this._known.Add(this.ComputeNext(this._known[^1]));
                return true;//no end
            }
        
            private long ComputeNext(long lastKnown) {
                long current = lastKnown + 2;//start at 2 past last known value, which is guaranteed odd because we initialize up thru 3
        
                int testIdx;
                long sqrt;
                bool isComposite;
                while (true) {//keep going until a new prime is found
                    testIdx = 1;//all test values are odd, so skip testing the first known prime (two)
                    sqrt = (long)Math.Sqrt(current);//round down, and avoid casting due to the comparison type of the while loop condition
        
                    isComposite = false;
                    while (this._known[testIdx] <= sqrt) {
                        if (current % this._known[testIdx++] == 0L) {
                            isComposite = true;
                            break;
                        }
                    }
        
                    if (isComposite)
                        current += 2;
                    else
                        return current;//and end
                }
            }
        
            public void Reset() {
                this._currentIdx = -1;
            }
            public void Dispose() {
                this._known = null;
            }
        }
        

        值和索引的 Int64

        注意访问_known 列表的索引的必要转换。

        class PrimeEnumeratorInt64Indices : IEnumerator<long> {
            public long Current { get { return this._known[(int)this._currentIdx]; } }
            object IEnumerator.Current { get { return this.Current; } }
        
            private long _currentIdx = -1;
            private List<long> _known = new List<long>() { 2, 3 };
        
            public bool MoveNext() {
                if (++this._currentIdx >= this._known.Count)
                    this._known.Add(this.ComputeNext(this._known[^1]));
                return true;//no end
            }
        
            private long ComputeNext(long lastKnown) {
                long current = lastKnown + 2;//start at 2 past last known value, which is guaranteed odd because we initialize up thru 3
        
                long testIdx;
                long sqrt;
                bool isComposite;
                while (true) {//keep going until a new prime is found
                    testIdx = 1;//all test values are odd, so skip testing the first known prime (two)
                    sqrt = (long)Math.Sqrt(current);//round down, and avoid casting due to the comparison type of the while loop condition
        
                    isComposite = false;
                    while (this._known[(int)testIdx] <= sqrt) {
                        if (current % this._known[(int)testIdx++] == 0L) {
                            isComposite = true;
                            break;
                        }
                    }
        
                    if (isComposite)
                        current += 2;
                    else
                        return current;//and end
                }
            }
        
            public void Reset() {
                this._currentIdx = -1;
            }
            public void Dispose() {
                this._known = null;
            }
        }
        

        总的来说,由于List&lt;...&gt; _known 集合,我的测试程序在 20 秒后对 Int32 使用 43MB 内存,对 Int64 使用 75MB 内存,这是我观察到的最大差异。


        我也使用无符号类型分析了版本。这是我的结果(发布模式):

        Testing PrimeEnumeratorInt32
                Total time is 20000 ms, number of computed primes is 3842603
        Testing PrimeEnumeratorUInt32
                Total time is 20001 ms, number of computed primes is 3841554
        Testing PrimeEnumeratorInt64
                Total time is 20001 ms, number of computed primes is 3839953
        Testing PrimeEnumeratorUInt64
                Total time is 20002 ms, number of computed primes is 3837199
        

        所有 4 个版本的性能基本相同。我想这里的教训是永远不要假设性能会受到怎样的影响,如果你的目标是 x64 架构,你应该可能使用Int64,因为它与我的Int32 版本匹配,即使使用内存使用量增加。

        验证我的主要计算器正在工作:

        附:发布模式的结果一致,速度提高了 1.1%。

        附言以下是必要的using 语句:

        using System;
        using System.Collections;
        using System.Collections.Generic;
        using System.Diagnostics;
        using System.Linq;
        using System.Reflection;
        using System.Threading;
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2015-09-08
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2013-11-21
          • 1970-01-01
          • 2021-03-21
          • 1970-01-01
          相关资源
          最近更新 更多