比起 <,<= 是否会更慢?

4
我在SO上找到了一些关于<和<=的性能比较的问题(其中一个被严重踩),并且我总是发现相同的答案,即两者之间没有性能差异。
我写了一个程序来进行比较(这里提供了一个不太好用的fiddle...复制到你的机器上运行),其中我创建了两个循环for (int i = 0; i <= 1000000000; i++ )for (int i = 0; i < 1000000001; i++ )在两个不同的方法中。
我运行了每种方法100次;取平均经过时间,发现使用<=操作符的循环速度比使用<操作符的慢。
我多次运行该程序,并且<=总是需要更长的时间才能完成。 我的结果(以毫秒为单位)是:
3018.73, 2778.22

2816.87,2760.62

2859.02,2797.05

我的问题是:如果两者都不更快,为什么结果有差异?我的程序有什么问题吗?


9
你确定你知道如何编写微基准测试吗?你已经以发布模式编译过了吗?在没有调试器的情况下运行(按 CTRL + F5)?设置进程高优先级?而且这只是一个空的 for 循环...在发布模式下它可能会被优化掉...所以你需要在里面添加一些代码。 - xanatos
6
你是如何进行度量的?有考虑 JIT 时间吗?你是使用 Stopwatch 还是 DateTime?在程序整体上,这个运算符的2%差异会对你的程序产生很大影响吗?它们给出不同的结果 - 如果其中一个更快,为什么会很重要呢? - D Stanley
5
科学!即使它对你的程序没有任何影响,你怎么能忍受“不知道”的感觉呢? - Kevin Gosse
3
另外,请确保不要犯下这些基准测试错误 - Corak
3
他只是好奇为什么会得到不同的结果,他并没有意味着那真的有关系。 - Feign
显示剩余11条评论
2个回答

10

基准测试是一门精湛的艺术。你所描述的不可能在物理上实现,<=和<运算符只会生成执行速度完全相同的不同处理器指令。我稍微修改了你的程序,将for()循环中的两个零删除,使我不必永远等待,然后运行了十次DoIt:

x86抖动:

Less Than Equal To Method Time Elapsed: 0.5
Less Than Method Time Elapsed: 0.42
Less Than Equal To Method Time Elapsed: 0.36
Less Than Method Time Elapsed: 0.46
Less Than Equal To Method Time Elapsed: 0.4
Less Than Method Time Elapsed: 0.34
Less Than Equal To Method Time Elapsed: 0.33
Less Than Method Time Elapsed: 0.35
Less Than Equal To Method Time Elapsed: 0.35
Less Than Method Time Elapsed: 0.32
Less Than Equal To Method Time Elapsed: 0.32
Less Than Method Time Elapsed: 0.32
Less Than Equal To Method Time Elapsed: 0.34
Less Than Method Time Elapsed: 0.32
Less Than Equal To Method Time Elapsed: 0.32
Less Than Method Time Elapsed: 0.31
Less Than Equal To Method Time Elapsed: 0.34
Less Than Method Time Elapsed: 0.32
Less Than Equal To Method Time Elapsed: 0.31
Less Than Method Time Elapsed: 0.32

x64抖动:

Less Than Equal To Method Time Elapsed: 0.44
Less Than Method Time Elapsed: 0.4
Less Than Equal To Method Time Elapsed: 0.44
Less Than Method Time Elapsed: 0.45
Less Than Equal To Method Time Elapsed: 0.36
Less Than Method Time Elapsed: 0.35
Less Than Equal To Method Time Elapsed: 0.38
Less Than Method Time Elapsed: 0.34
Less Than Equal To Method Time Elapsed: 0.33
Less Than Method Time Elapsed: 0.34
Less Than Equal To Method Time Elapsed: 0.34
Less Than Method Time Elapsed: 0.32
Less Than Equal To Method Time Elapsed: 0.32
Less Than Method Time Elapsed: 0.35
Less Than Equal To Method Time Elapsed: 0.32
Less Than Method Time Elapsed: 0.42
Less Than Equal To Method Time Elapsed: 0.32
Less Than Method Time Elapsed: 0.31
Less Than Equal To Method Time Elapsed: 0.32
Less Than Method Time Elapsed: 0.35

你能从中得到的唯一真正信号就是第一个DoIt()执行缓慢,这也可以在测试结果中看到,这是抖动开销。而最重要的信号则是嘈杂。两个循环的中位数大致相等,标准差相当大。

否则,当你微调时,你总是会得到这种信号,代码执行并不非常确定。除了.NET运行时开销通常很容易消除之外,你的程序不是唯一在你的计算机上运行的程序。它必须共享处理器,只有WriteLine()调用已经产生影响。由conhost.exe进程执行,在测试代码进入下一个for()循环时并行运行。还有其他在你的机器上发生的所有事情,内核代码和中断处理程序也有它们的轮换。

代码生成也可能起作用,例如你应该做的一件事就是交换这两个调用。总的来说,处理器本身执行代码非常不确定。处理器高速缓存的状态以及分支预测逻辑收集的历史数据量都非常重要。

当我进行基准测试时,我认为15%或更小的差异在统计学上没有显著意义。追踪少于那个值的差异相当困难,你必须非常仔细地研究机器代码。像分支目标错位或变量没有存储在处理器寄存器中这样的愚蠢错误会导致执行时间的大幅影响。这不是你可以修复的,抖动没有足够的旋钮来调整。


感谢您的解释 :) - Dumbledore

4
首先,即使正确执行基准测试,也有很多原因会导致结果存在差异。以下是一些可能的原因:
- 您的计算机同时运行许多其他进程,切换上下文等。操作系统不断接收和处理来自各种I/O设备的中断等。所有这些都可能导致计算机暂停的时间超过您正在测试的实际代码的运行时间。
- JIT(Just-In-Time)编译器可以检测到函数已运行一定次数,并根据此信息应用额外的优化。例如,循环展开可以大大减少程序需要进行的跳转次数,而跳转比典型的CPU操作昂贵得多。重新优化指令需要时间,但之后会加速运行。
- 您的硬件正在尝试进行额外的优化,例如分支预测,以确保其流水线被尽可能有效地使用。(如果猜测正确,它可以在等待<<=比较完成时假装执行i++,然后在发现错误时丢弃结果。)这些优化的影响取决于许多因素,不容易预测。
其次,实际上很难进行良好的基准测试。以下是我一直在使用的基准测试模板。它并不完美,但它确保任何出现的模式不太可能基于执行顺序或随机机会:
/* This is a benchmarking template I use in LINQPad when I want to do a
 * quick performance test. Just give it a couple of actions to test and
 * it will give you a pretty good idea of how long they take compared
 * to one another. It's not perfect: You can expect a 3% error margin
 * under ideal circumstances. But if you're not going to improve
 * performance by more than 3%, you probably don't care anyway.*/
void Main()
{
    // Enter setup code here
    var actions = new[]
    {
        new TimedAction("control", () =>
        {
            int i = 0;
        }),
        new TimedAction("<", () =>
        {
           for (int i = 0; i < 1000001; i++)
            {}
        }),
        new TimedAction("<=", () =>
        {
           for (int i = 0; i <= 1000000; i++)
            {}
        }),
        new TimedAction(">", () =>
        {
           for (int i = 1000001; i > 0; i--)
            {}
        }),
        new TimedAction(">=", () =>
        {
           for (int i = 1000000; i >= 0; i--)
            {}
        })
    };
    const int TimesToRun = 10000; // Tweak this as necessary
    TimeActions(TimesToRun, actions);
}


#region timer helper methods
// Define other methods and classes here
public void TimeActions(int iterations, params TimedAction[] actions)
{
    Stopwatch s = new Stopwatch();
    int length = actions.Length;
    var results = new ActionResult[actions.Length];
    // Perform the actions in their initial order.
    for(int i = 0; i < length; i++)
    {
        var action = actions[i];
        var result = results[i] = new ActionResult{Message = action.Message};
        // Do a dry run to get things ramped up/cached
        result.DryRun1 = s.Time(action.Action, 10);
        result.FullRun1 = s.Time(action.Action, iterations);
    }
    // Perform the actions in reverse order.
    for(int i = length - 1; i >= 0; i--)
    {
        var action = actions[i];
        var result = results[i];
        // Do a dry run to get things ramped up/cached
        result.DryRun2 = s.Time(action.Action, 10);
        result.FullRun2 = s.Time(action.Action, iterations);
    }
    results.Dump();
}

public class ActionResult
{
    public string Message {get;set;}
    public double DryRun1 {get;set;}
    public double DryRun2 {get;set;}
    public double FullRun1 {get;set;}
    public double FullRun2 {get;set;}
}

public class TimedAction
{
    public TimedAction(string message, Action action)
    {
        Message = message;
        Action = action;
    }
    public string Message {get;private set;}
    public Action Action {get;private set;}
}

public static class StopwatchExtensions
{
    public static double Time(this Stopwatch sw, Action action, int iterations)
    {
        sw.Restart();
        for (int i = 0; i < iterations; i++)
        {
            action();
        }
        sw.Stop();

        return sw.Elapsed.TotalMilliseconds;
    }
}
#endregion

当我在LINQPad中运行时,得到的结果如下:

benchmark result

因此,您会注意到存在一些变化,特别是在早期阶段,但是在反复运行所有内容之后,没有明显的模式出现表明一种方式比另一种方式快或慢。


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