显示向uint转换的代码示例比范围检查更有效



所以我正在研究这个问题,普遍的共识是uint cast版本比0的范围检查更有效。由于该代码也在MS的List实现中,我认为这是一个真正的优化。然而,我未能为uint版本生成更好性能的代码示例。我尝试了不同的测试,但有一些东西丢失了,或者我的代码的其他部分使检查时间相形见绌。我的最后一次尝试是这样的:

class TestType
{
    public TestType(int size)
    {
        MaxSize = size;
        Random rand = new Random(100);
        for (int i = 0; i < MaxIterations; i++)
        {
            indexes[i] = rand.Next(0, MaxSize);
        }
    }
    public const int MaxIterations = 10000000;
    private int MaxSize;
    private int[] indexes = new int[MaxIterations];
    public void Test()
    {
        var timer = new Stopwatch();
        int inRange = 0;
        int outOfRange = 0;
        timer.Start();
        for (int i = 0; i < MaxIterations; i++)
        {
            int x = indexes[i];
            if (x < 0 || x > MaxSize)
            {
                throw new Exception();
            }
            inRange += indexes[x];
        }
        timer.Stop();
        Console.WriteLine("Comparision 1: " + inRange + "/" + outOfRange + ", elapsed: " + timer.ElapsedMilliseconds + "ms");
        inRange = 0;
        outOfRange = 0;
        timer.Reset();
        timer.Start();
        for (int i = 0; i < MaxIterations; i++)
        {
            int x = indexes[i];
            if ((uint)x > (uint)MaxSize)
            {
                throw new Exception();
            }
            inRange += indexes[x];
        }
        timer.Stop();
        Console.WriteLine("Comparision 2: " + inRange + "/" + outOfRange + ", elapsed: " + timer.ElapsedMilliseconds + "ms");
    }
}
class Program
{
    static void Main()
    {
        TestType t = new TestType(TestType.MaxIterations);
        t.Test();
        TestType t2 = new TestType(TestType.MaxIterations);
        t2.Test();
        TestType t3 = new TestType(TestType.MaxIterations);
        t3.Test();
    }
}

代码有点乱,因为我尝试了很多方法来加快uint检查的执行速度,比如将比较变量移动到类的字段中,生成随机索引访问等等,但在每种情况下,两个版本的结果似乎都是一样的。那么,这种变化适用于现代x86处理器吗?有人能以某种方式证明吗?

请注意,我并不是要求有人来修复我的样本或解释它的问题。我只是想看看优化是否有效。

       if (x < 0 || x > MaxSize)

比较由CMP处理器指令(比较)执行。你会想看看Agner Fog的指令表文档(PDF),它列出了指令的成本。在列表中找到您的处理器,然后找到CMP指令。

对于我的Haswell,CMP需要1个周期的延迟和0.25个周期的吞吐量。

Haswell有4个整数执行单元,可以同时执行指令。当一个程序包含足够多的整数运算(如CMP)而没有相互依赖性时,它们就可以同时执行。实际上使程序速度提高了4倍。你并不总是设法让它们4个同时忙于你的代码,这实际上是非常罕见的。但在这种情况下,你确实让他们中的两个忙起来了。或者换句话说,两次比较所花费的时间与一次1个周期的时间一样长。

还有其他因素在起作用,使执行时间相同。有一点很有帮助,那就是处理器可以很好地预测分支,它可以推测性地执行x > MaxSize,而不考虑短路评估。事实上,它最终会使用结果,因为分支从未被占用。

这段代码中真正的瓶颈是数组索引,访问内存是处理器能做的最慢的事情之一。因此,"快速"版本的代码并不更快,尽管它提供了更多的机会让处理器同时执行指令。不管怎么说,今天的机会不大,处理器的执行单元太多,无法保持忙碌。否则就是使超线程工作的功能。在这两种情况下,处理器都以相同的速度出现故障。

在我的机器上,我必须编写比4个引擎占用更多的代码,以使速度变慢。像这样愚蠢的代码:

     if (x < 0 || x > MaxSize || x > 10000000 || x > 20000000 || x > 3000000) {
         outOfRange++; 
     }
     else {
         inRange++;
     }

使用5个比较,现在我可以得出差异,61 vs 47毫秒。或者换句话说,这是一种计算处理器中整数引擎数量的方法。呵呵:)

因此,这是一个微优化,可能在十年前就得到了回报。现在不行了。把它从你需要担心的事情列表中划掉:)

我建议尝试在index超出范围时不引发异常的代码。例外情况非常昂贵,而且可能会完全打乱你的替补结果。

下面的代码对1000000个结果的1000次迭代进行了定时平均测试。

using System;
using System.Diagnostics;
namespace BenchTest
{
    class Program
    {
        const int LoopCount = 1000000;
        const int AverageCount = 1000;
        static void Main(string[] args)
        {
            Console.WriteLine("Starting Benchmark");
            RunTest();
            Console.WriteLine("Finished Benchmark");
            Console.Write("Press any key to exit...");
            Console.ReadKey();
        }
        static void RunTest()
        {
            int cursorRow = Console.CursorTop; int cursorCol = Console.CursorLeft;
            long totalTime1 = 0; long totalTime2 = 0;
            long invalidOperationCount1 = 0; long invalidOperationCount2 = 0;
            for (int i = 0; i < AverageCount; i++)
            {
                Console.SetCursorPosition(cursorCol, cursorRow);
                Console.WriteLine("Running iteration: {0}/{1}", i + 1, AverageCount);
                int[] indexArgs = RandomFill(LoopCount, int.MinValue, int.MaxValue);
                int[] sizeArgs = RandomFill(LoopCount, 0, int.MaxValue);
                totalTime1 += RunLoop(TestMethod1, indexArgs, sizeArgs, ref invalidOperationCount1);
                totalTime2 += RunLoop(TestMethod2, indexArgs, sizeArgs, ref invalidOperationCount2);
            }
            PrintResult("Test 1", TimeSpan.FromTicks(totalTime1 / AverageCount), invalidOperationCount1);
            PrintResult("Test 2", TimeSpan.FromTicks(totalTime2 / AverageCount), invalidOperationCount2);
        }
        static void PrintResult(string testName, TimeSpan averageTime, long invalidOperationCount)
        {
            Console.WriteLine(testName);
            Console.WriteLine("    Average Time: {0}", averageTime);
            Console.WriteLine("    Invalid Operations: {0} ({1})", invalidOperationCount, (invalidOperationCount / (double)(AverageCount * LoopCount)).ToString("P3"));
        }
        static long RunLoop(Func<int, int, int> testMethod, int[] indexArgs, int[] sizeArgs, ref long invalidOperationCount)
        {
            Stopwatch sw = new Stopwatch();
            Console.Write("Running {0} sub-iterations", LoopCount);
            sw.Start();
            long startTickCount = sw.ElapsedTicks;
            for (int i = 0; i < LoopCount; i++)
            {
                invalidOperationCount += testMethod(indexArgs[i], sizeArgs[i]);
            }
            sw.Stop();
            long stopTickCount = sw.ElapsedTicks;
            long elapsedTickCount = stopTickCount - startTickCount;
            Console.WriteLine(" - Time Taken: {0}", new TimeSpan(elapsedTickCount));
            return elapsedTickCount;
        }
        static int[] RandomFill(int size, int minValue, int maxValue)
        {
            int[] randomArray = new int[size];
            Random rng = new Random();
            for (int i = 0; i < size; i++)
            {
                randomArray[i] = rng.Next(minValue, maxValue);
            }
            return randomArray;
        }
        static int TestMethod1(int index, int size)
        {
            return (index < 0 || index >= size) ? 1 : 0;
        }
        static int TestMethod2(int index, int size)
        {
            return ((uint)(index) >= (uint)(size)) ? 1 : 0;
        }
    }
}

您不是在比较同类。

您所说的代码不仅通过使用优化保存了一个分支,而且在一个小方法中保存了4字节的CIL。

在一个小方法中,4个字节可能是内联和不内联的区别。

如果调用该方法的方法也被写得很小,那么这可能意味着两个(或多个)方法调用被转换为一段内联代码。

也许其中一些是这样的,因为它是内联的,可以通过抖动进行分析,因此再次进行了进一步优化。

真正的区别不在index < 0 || index >= _size(uint)index >= (uint)_size之间,而是在反复努力最小化方法体大小的代码和没有这样做的代码之间。例如,如果必要的话,看看如何使用另一个方法来抛出异常,从而进一步减少几个字节的CIL。

(不,这并不是说我认为所有方法都应该这样写,但这样做肯定会有性能差异)。

最新更新