You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何C#中用Stopwatch测试时首个计时函数总是更快?

问题:C#迭代与递归性能测试中调用顺序影响耗时的原因?

我想要验证C#中迭代与递归哪种方式执行更快,但在简单测试中使用Stopwatch无法得到可信结果。

测试代码包含两个实现阶乘功能的函数,分别采用迭代和递归方式:

internal class Program
{
    // 阶乘定义:从1到该数的所有整数乘积,0! = 1,负数无阶乘
    static void Main(string[] args)
    {
        Console.WriteLine("Please Enter a Number:");
        int number = Convert.ToInt32(Console.ReadLine());
        
        double sw1Elapsed = 0d;
        double sw2Elapsed = 0d;
        double factorial = 0d;
        double factorial2 = 0d;

        Stopwatch sw = new Stopwatch();
        sw.Reset(); 
        for (int i = 0; i < 6; i++)
        {
            sw.Restart();
            factorial = Factorial(number);
            sw.Stop();
            sw1Elapsed += sw.Elapsed.TotalMilliseconds;

            sw.Restart();
            factorial2 = FactorialByRecursion(number);
            sw.Stop();
            sw2Elapsed += sw.Elapsed.TotalMilliseconds;

            Console.WriteLine("Iteration:");
            Console.WriteLine($"Factorial of {number} = {factorial}");
            Console.WriteLine($"Iteration Time taken = {sw1Elapsed}");
            Console.WriteLine();
            Console.WriteLine("Recursion:");
            Console.WriteLine($"Factorial of {number} = {factorial2}");
            Console.WriteLine($"Recursion Time taken = {sw2Elapsed}");
            Console.WriteLine("-----------------------------------------------------");
        }
    }

    public static double Factorial(int number)
    {
        if (number == 0)
            return 1;

        double factorial = 1;
        for (int i = number; i >= 1; i--)
        {
            factorial *= i;
        }
        return factorial;
    }

    public static double FactorialByRecursion(int number)
    {
        if (number == 0)
            return 1;
        return number * FactorialByRecursion(number - 1);
    }
}

首次测试(迭代先调用)结果显示递归总耗时更低:

Please Enter a Number: 6 
Iteration: Factorial of 6 = 720 Iteration Time taken = 0.3171
Recursion: Factorial of 6 = 720 Recursion Time taken = 0.0616
-----------------------------------------------------
Iteration: Factorial of 6 = 720 Iteration Time taken = 0.3172
Recursion: Factorial of 6 = 720 Recursion Time taken = 0.061700000000000005
-----------------------------------------------------
...(后续循环耗时增量极小)

当调整调用顺序,先调用递归函数时,迭代总耗时反而更低:

Please Enter a Number:
6
Iteration:
Factorial of 6 = 720
Iteration Time taken = 0.0571

Recursion:
Factorial of 6 = 720
Recursion Time taken = 0.1066
-----------------------------------------------------
Iteration:
Factorial of 6 = 720
Iteration Time taken = 0.0571

Recursion:
Factorial of 6 = 720
Recursion Time taken = 0.1069
-----------------------------------------------------
...(后续循环耗时增量极小)

我发现无论将哪个函数放在循环中第一个调用,后调用的函数总耗时总是更优,请问这一差异产生的原因是什么?(注:使用int类型存储阶乘结果时也会出现相同现象)


解答

核心原因:即时编译(JIT)与测试方法缺陷

你的测试结果异常,主要是两个因素共同作用:

  1. JIT编译的首次调用开销
    .NET程序运行时,方法不会提前编译为机器码,而是在第一次被调用时由CLR的JIT编译器将IL代码翻译为机器码,这个编译过程会产生额外耗时。

    • 第一个被调用的函数,其首次调用时间包含了JIT编译的完整开销;
    • 第二个被调用的函数,虽然也是首次调用,但此时CLR的编译环境已经完成初始化,JIT编译的耗时会显著降低,导致首次调用时间远低于第一个函数。
  2. 测试方法的局限性

    • 样本量太小:仅6次循环,首次调用的高开销会直接主导总耗时的统计结果,无法反映方法本身的执行性能;
    • 连续调用的干扰:同一循环内连续调用两个函数,CPU缓存、JIT预热的交叉影响会进一步扭曲测试数据;
    • 累加方式误导:你将每次调用的时间累加后展示,没有区分首次调用和后续调用的差异,导致总耗时的对比失去参考价值。

另外,从理论上来说,同逻辑的迭代性能本就优于递归——递归每次调用都需要创建栈帧、保存上下文,这些额外开销在大量调用时会被放大,但你的测试因为JIT的干扰,完全掩盖了这一点。

如何获得可信的性能对比结果

要准确测试两者的性能,需要优化测试流程:

  • 预热消除JIT开销:正式计时前,先分别调用两个函数一次,触发JIT编译,避免首次调用的编译时间影响测试结果;
  • 增大样本量:将循环次数提升到万级甚至十万级,让统计结果更稳定;
  • 分开独立测试:不要在同一个循环内连续测试两个函数,分开循环测试,避免CPU缓存等因素的交叉干扰;
  • 统计平均耗时:计算总耗时除以调用次数,得到单次调用的平均时间,而非累加总耗时。

优化后的示例代码:

static void Main(string[] args)
{
    Console.WriteLine("Please Enter a Number:");
    int number = Convert.ToInt32(Console.ReadLine());

    // 预热:触发JIT编译,消除首次调用开销
    Factorial(number);
    FactorialByRecursion(number);

    const int testCount = 100000;
    Stopwatch sw = new Stopwatch();

    // 测试迭代性能
    sw.Restart();
    for (int i = 0; i < testCount; i++)
    {
        Factorial(number);
    }
    sw.Stop();
    Console.WriteLine($"迭代单次平均耗时:{sw.Elapsed.TotalMilliseconds / testCount:F6} ms");

    // 测试递归性能
    sw.Restart();
    for (int i = 0; i < testCount; i++)
    {
        FactorialByRecursion(number);
    }
    sw.Stop();
    Console.WriteLine($"递归单次平均耗时:{sw.Elapsed.TotalMilliseconds / testCount:F6} ms");
}

使用这种方法测试后,就能明显看到迭代的性能优于递归,因为递归的栈帧开销会在大量调用中体现出来。


内容的提问来源于stack exchange,提问作者D.Man

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 07:02:32