在背靠背for循环中的int,short,byte性能

时间:2010-04-07 16:09:36

标签: c# performance types for-loop

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

为了满足我自己对使用“适当大小”整数与“优化”整数的优缺点的好奇心,我编写了以下代码,强化了我以前在.Net中对int性能所做的事情(并解释了在上面的链接中),它是针对int性能优化而不是短或字节。

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

我发现令人费解。有人可以提供解释吗?

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

8 个答案:

答案 0 :(得分:39)

首先,它不是针对int性能优化的.NET,而是优化的机器,因为32位是本机字大小(除非你在x64上,在哪种情况下它是long或64位)。

其次,你在每个循环中写入控制台 - 这比增加和测试循环计数器要昂贵得多,所以你不会在这里测量任何真实的东西。

第三,byte的范围最大为255,所以你可以循环254次(如果你试图做255,它会溢出而循环永远不会结束 - 但你不需要停在128 )。

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

最后,在计算中使用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
  Int Loop:Elapsed Time = 00:00:03.2986990

所以,没有明显的区别。

另外,请确保您在发布模式下进行配置,很多人会忘记并在调试模式下进行测试,这将显着降低准确性。

答案 1 :(得分:12)

这段时间大部分时间都花在写入控制台上。尝试在循环中做一些不同的事情......

此外:

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

这是我的基准:

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

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

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

答案 2 :(得分:4)

出于好奇,我修改了Aaronaught的程序,并在x86和x64两种模式下编译。 Strange,Int在x64中运行得更快:

86

字节循环:经过时间= 00:00:00.8636454
短循环:经过时间= 00:00:00.8795518
UShort循环:经过时间= 00:00:00.8630357
Int Loop:经过时间= 00:00:00.5184154
UInt循环:经过时间= 00:00:00.4950156
长循环:经过时间= 00:00:01.2941183
ULONG循环:经过时间= 00:00:01.3023409

64

字节循环:经过时间= 00:00:01.0646588
短循环:经过时间= 00:00:01.0719330
UShort循环:经过时间= 00:00:01.0711545
Int Loop:经过时间= 00:00:00.2462848
UInt循环:经过时间= 00:00:00.4708777
长循环:经过时间= 00:00:00.5242272
ULONG循环:经过时间= 00:00:00.5144035

答案 3 :(得分:2)

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

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

整数更快

Jon的输出

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

没有任何东西

Jon在结果中调用tostring有一个很大的固定常量,如果在循环中完成的工作较少,可能会隐藏可能带来的好处。 Aaronaught正在使用32位操作系统,这似乎不像我正在使用的x64钻机那样使用整数。

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

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

<强>仿形 Redgate / Jetbrains / Slimtune / CLR分析器和我自己的分析器都表明结果是正确的。

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

答案 4 :(得分:1)

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

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

基准

.NET Core 3.1配置定位Release (Any CPU)。在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>();
        }
    }
}

答案 5 :(得分:0)

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

答案 6 :(得分:0)

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

建议:位移,乘法,数组操作,加法,还有很多其他......

答案 7 :(得分:0)

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

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

正如其他人指出的那样,您的短循环使您的结果(尤其是调试语句)难以理解。您应该尝试使用辅助线程。


以下是intlong的性能比较:

enter image description here

当然,请确保避免使用long(以及普通int以外的任何其他方法)作为数组索引,因为您甚至无法使用它们,并且强制转换为int损害了性能(在我的测试中无法衡量)。

enter image description here

这是我的性能分析代码,它在工作线程永远旋转时轮询进度。通过反复测试,它的确会稍微减慢速度,因此,我确保同时以其他顺序和单独的顺序进行测试:

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 SleepExtensions.SleepWithProgress", 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()) { }
}

}

请注意,您可以将Thread.Sleep替换为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; } }

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

Int32版本

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

值和索引均为Int64

请注意访问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<...> _known

总计,由于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 的收集,我的测试程序在20秒后为Int32使用了43MB的内存,为Int64使用了75MB的内存,这是我观察到的最大差异。


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

Int64

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

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

enter image description here

P.S。发布模式具有一致的结果,速度提高了1.1%。

P.P.S。以下是必要的using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Threading; 语句:

.flier {
    pointer-events: none;
}

.flier > * {
/* Adjust animation duration to change the element’s speed */
        animation: fly 200s linear infinite;
        pointer-events: none !important;
    top: 0;
    left: 0;
    transform: translateX(-120%) translateY(-120%) rotateZ(0);
    position: fixed;
    animation-delay: 0s;
    z-index: -1;
    /*z-index: 999999;*/
}


@keyframes fly {

    98.001%, 0% {
                display: block;
        transform: translateX(-200%) translateY(100vh) rotateZ(0deg)
    }

    15% {
        transform: translateX(100vw) translateY(-100%) rotateZ(180deg)
    }

    15.001%, 18% {
        transform: translateX(100vw) translateY(-30%) rotateZ(0deg)
    }

    40% {
        transform: translateX(-200%) translateY(3vh) rotateZ(-180deg)
    }

    40.001%, 43% {
        transform: translateX(-200%) translateY(-100%) rotateZ(-180deg)
    }

    65% {
        transform: translateX(100vw) translateY(50vh) rotateZ(0deg)
    }

    65.001%, 68% {
        transform: translateX(20vw) translateY(-200%) rotateZ(180deg)
    }
    95% {
        transform: translateX(10vw) translateY(100vh) rotateZ(0deg)
    }
}