RError.com

RError.com Logo RError.com Logo

RError.com Navigation

  • 主页

Mobile menu

Close
  • 主页
  • 系统&网络
    • 热门问题
    • 最新问题
    • 标签
  • Ubuntu
    • 热门问题
    • 最新问题
    • 标签
  • 帮助
主页 / 问题 / 1427600
Accepted
i-one
i-one
Asked:2022-09-07 22:32:59 +0000 UTC2022-09-07 22:32:59 +0000 UTC 2022-09-07 22:32:59 +0000 UTC

与循环无关的代码会减慢它的速度

  • 772

我遇到了一个不寻常的情况。在与它无关的循环之前添加的一行代码会减慢循环速度。这是“简化形式”的样子。

我有一个原始的循环方法:

int[] array = new int[ElemCnt];

public int Sum()
{
    int sum = 0;

    for (int i = 0; i < array.Length; i++)
        sum += array[i];

    return sum;
}

然后将此方法修改如下:

long state = 1;

public int Sum2()
{
    int sum = 0;

    if (Interlocked.Read(ref state) == 0)
        return sum;

    for (int i = 0; i < array.Length; i++)
        sum += array[i];

    return sum;
}

那些。它只是添加了一个循环前检查。

假设这个检查的执行时间可以通过某个常数来估计是合乎逻辑的。第二种方法(结果最差)的执行时间可以估计T2 ≈ T + C为 随着数组长度的增长,显然这两种方法的执行时间将越来越难以区分()。TCT2 ≈ T

但是,这不会发生:

BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19044.1766 (21H2)
Intel Core i5-4690 CPU 3.50GHz (Haswell), 1 CPU, 4 logical and 4 physical cores
.NET SDK=6.0.301
  [Host]     : .NET 6.0.6 (6.0.622.26707), X64 RyuJIT
  DefaultJob : .NET 6.0.6 (6.0.622.26707), X64 RyuJIT
方法 元件 意思是 错误 标准差 比率 比率标准差
和 1000 590.1ns 6.22ns 5.81ns 1.00 0.00
总和2 1000 1,896.4ns 9.88ns 9.24ns 3.21 0.03
和 10000 5,729.5ns 27.40ns 24.29ns 1.00 0.00
总和2 10000 18.929.2ns 208.10ns 194.66ns 3.30 0.03
和 100000 57.273.8ns 258.89ns 242.17ns 1.00 0.00
总和2 100000 187.707.1ns 2,121.93ns 1,984.85ns 3.28 0.03
和 1000000 590.207.1ns 9,395.29ns 8,328.68ns 1.00 0.00
总和2 1000000 1,943,186.9ns 34.793.55ns 32.545.90ns 3.29 0.06

那些。它确实有效T2 ≈ K * T!

这表明循环在第二种情况下运行较慢。就好像添加的检查以某种方式减慢了循环。这是怎么回事?


基准代码:

public class LoopBench
{
    int[] array = null;
    long state = 1;

    [Params(1000, 10000, 100000, 1000000)]
    public int ElemCnt { get; set; }

    [GlobalSetup]
    public void Setup()
    {
        array = new int[ElemCnt];
    }

    [Benchmark(Baseline = true)]
    public int Sum()
    {
        int sum = 0;

        for (int i = 0; i < array.Length; i++)
            sum += array[i];

        return sum;
    }

    [Benchmark]
    public int Sum2()
    {
        int sum = 0;

        if (Interlocked.Read(ref this.state) == 0)
            return sum;

        for (int i = 0; i < array.Length; i++)
            sum += array[i];

        return sum;
    }
}
c# .net
  • 1 1 个回答
  • 71 Views

1 个回答

  • Voted
  1. Best Answer
    i-one
    2022-09-07T22:32:59Z2022-09-07T22:32:59Z

    这是怎么回事?

    如果我们比较这两种方法的 IL 代码,则看不到任何罪犯。

    这是第二种方法的 IL 的样子:

    .maxstack  3
    .locals init (int32 V_0, int32 V_1)
    IL_0000:  ldc.i4.0
    IL_0001:  stloc.0
    IL_0002:  ldarg.0
    IL_0003:  ldflda     int64 LoopBench::state
    IL_0008:  call       int64 Interlocked::Read(int64&)
    IL_000d:  brtrue.s   IL_0011
    IL_000f:  ldloc.0
    IL_0010:  ret
    IL_0011:  ldc.i4.0
    IL_0012:  stloc.1
    IL_0013:  br.s       IL_0024
    IL_0015:  ldloc.0
    IL_0016:  ldarg.0
    IL_0017:  ldfld      int32[] LoopBench::'array'
    IL_001c:  ldloc.1
    IL_001d:  ldelem.i4
    IL_001e:  add
    IL_001f:  stloc.0
    IL_0020:  ldloc.1
    IL_0021:  ldc.i4.1
    IL_0022:  add
    IL_0023:  stloc.1
    IL_0024:  ldloc.1
    IL_0025:  ldarg.0
    IL_0026:  ldfld      int32[] LoopBench::'array'
    IL_002b:  ldlen
    IL_002c:  conv.i4
    IL_002d:  blt.s      IL_0015
    IL_002f:  ldloc.0
    IL_0030:  ret
    

    对于第一种方法,我不提供 IL 代码,因为 它几乎相同,看起来好像从IL_0002到IL_0010 的指令被从第二种方法的代码中抛出(实际检查和返回)。至于循环,特别是循环体(从IL_0015到IL_002d)没有区别。

    但 JIT 生成的代码略有不同。并且在循环体中同样存在差异。

    第一种方法的代码,如果你把所有次要的东西都扔掉,看起来像这样:

    ; LoopBench.Sum()
           sub       rsp,28
           xor       eax,eax                        // sum = 0
           xor       edx,edx                        // i = 0
           ...
    M00_L00:
           ...
           movsxd    r9,edx                 
           add       eax,[r8+r9*4+10]               // sum += array[i]
           inc       edx                            // i++
           cmp       [rcx+8],edx
           jg        short M00_L00                  // array.Length > i
    M00_L01:
           add       rsp,28
           ret
    

    而第二种方法的代码(也没有小细节)是这样的:

    ; LoopBench.Sum2()
           sub       rsp,28
           xor       eax,eax                        // sum = 0
           mov       [rsp+24],eax                   // [stack] <- sum
           ...
    M00_L00:
           xor       eax,eax                        // i = 0
           ...
    M00_L01:
           ...
           movsxd    r8,eax
           mov       r9d,[rsp+24]                   // sum <- [stack]
           add       r9d,[rcx+r8*4+10]              // sum += array[i]
           inc       eax                            // i++
           cmp       [rdx+8],eax
           jg        short M00_L03                  // array.Length > i
    M00_L02:
           mov       eax,r9d                        // return sum
           add       rsp,28
           ret
    M00_L03:
           mov       [rsp+24],r9d                   // [stack] <- sum
           jmp       short M00_L01
    

    主要区别在于,在第二种情况下,在循环的每次迭代中,累加的和从堆栈中加载到寄存器 ( r9d ) 中,并在求和后存储回堆栈中。在第一种情况下,总和立即在寄存器(eax)中累加,根本不会进入堆栈。正是这种对堆栈的额外工作(连同内存访问比处理器寄存器慢的事实——即使可能存在缓存)导致第二种方法的性能下降。

    如果循环中有大量工作,则使用堆栈不会产生明显的效果。但这里很少。因此,这样的代码生成看起来很不典型,因为 编译器特别注意具有小主体的循环(它们的检测和优化)。

    好像添加的检查以某种方式减慢了循环

    不,检查本身不会减慢循环的执行速度。但它的存在会导致 JIT 为循环生成性能较低的代码。

    RyuJIT 编译器的这种行为是对以前版本的回归:

    BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19044.1766 (21H2)
    Intel Core i5-4690 CPU 3.50GHz (Haswell), 1 CPU, 4 logical and 4 physical cores
    .NET SDK=6.0.301
      [Host]             : .NET 6.0.6 (6.0.622.26707), X64 RyuJIT
      .NET 6.0           : .NET 6.0.6 (6.0.622.26707), X64 RyuJIT
      .NET 5.0           : .NET 5.0.17 (5.0.1722.21314), X64 RyuJIT
      .NET Core 3.1      : .NET Core 3.1.26 (CoreCLR 4.700.22.26002, CoreFX 4.700.22.26801), X64 RyuJIT
      .NET Core 2.2      : .NET Core 2.2.8 (CoreCLR 4.6.28207.03, CoreFX 4.6.28208.02), X64 RyuJIT
      .NET Framework 4.8 : .NET Framework 4.8 (4.8.4515.0), X64 RyuJIT
    
    方法 运行 意思是 错误 标准差 比率 比率标准差
    和 .NET 6.0 587.219.9ns 11,480.13ns 10.738.52ns 1.00 0.00
    总和2 .NET 6.0 1,943,794.5ns 15.662.22ns 13.078.66ns 3.33 0.04
    和 .NET 5.0 584.441.7ns 9,659.41ns 9.035.41ns 1.00 0.00
    总和2 .NET 5.0 1,955,357.4ns 18,500.14ns 16,399.89ns 3.35 0.06
    和 .NET 核心 3.1 597.269.8ns 8.984.42ns 8,404.03ns 1.00 0.00
    总和2 .NET 核心 3.1 1,987,871.5ns 35.514.92ns 33.220.67ns 3.33 0.09
    和 .NET 核心 2.2 585.353.0ns 7,527.64ns 7.041.36ns 1.00 0.00
    总和2 .NET 核心 2.2 576.804.1ns 2,975.21ns 2,637.45ns 0.99 0.01
    和 .NET 框架 4.8 580.485.7ns 5,634.76ns 5,270.76ns 1.00 0.00
    总和2 .NET 框架 4.8 591.283.8ns 8,772.34ns 8,205.65ns 1.02 0.02

    在.NET Framework 4.8和.NET Core 2.2中,事情还没有那么糟糕 :)

    我报告了“应该在哪里”的问题。也许它将在.NET的未来版本中得到修复。

    • 4

相关问题

Sidebar

Stats

  • 问题 10021
  • Answers 30001
  • 最佳答案 8000
  • 用户 6900
  • 常问
  • 回答
  • Marko Smith

    我看不懂措辞

    • 1 个回答
  • Marko Smith

    请求的模块“del”不提供名为“default”的导出

    • 3 个回答
  • Marko Smith

    "!+tab" 在 HTML 的 vs 代码中不起作用

    • 5 个回答
  • Marko Smith

    我正在尝试解决“猜词”的问题。Python

    • 2 个回答
  • Marko Smith

    可以使用哪些命令将当前指针移动到指定的提交而不更改工作目录中的文件?

    • 1 个回答
  • Marko Smith

    Python解析野莓

    • 1 个回答
  • Marko Smith

    问题:“警告:检查最新版本的 pip 时出错。”

    • 2 个回答
  • Marko Smith

    帮助编写一个用值填充变量的循环。解决这个问题

    • 2 个回答
  • Marko Smith

    尽管依赖数组为空,但在渲染上调用了 2 次 useEffect

    • 2 个回答
  • Marko Smith

    数据不通过 Telegram.WebApp.sendData 发送

    • 1 个回答
  • Martin Hope
    Alexandr_TT 2020年新年大赛! 2020-12-20 18:20:21 +0000 UTC
  • Martin Hope
    Alexandr_TT 圣诞树动画 2020-12-23 00:38:08 +0000 UTC
  • Martin Hope
    Air 究竟是什么标识了网站访问者? 2020-11-03 15:49:20 +0000 UTC
  • Martin Hope
    Qwertiy 号码显示 9223372036854775807 2020-07-11 18:16:49 +0000 UTC
  • Martin Hope
    user216109 如何为黑客设下陷阱,或充分击退攻击? 2020-05-10 02:22:52 +0000 UTC
  • Martin Hope
    Qwertiy 并变成3个无穷大 2020-11-06 07:15:57 +0000 UTC
  • Martin Hope
    koks_rs 什么是样板代码? 2020-10-27 15:43:19 +0000 UTC
  • Martin Hope
    Sirop4ik 向 git 提交发布的正确方法是什么? 2020-10-05 00:02:00 +0000 UTC
  • Martin Hope
    faoxis 为什么在这么多示例中函数都称为 foo? 2020-08-15 04:42:49 +0000 UTC
  • Martin Hope
    Pavel Mayorov 如何从事件或回调函数中返回值?或者至少等他们完成。 2020-08-11 16:49:28 +0000 UTC

热门标签

javascript python java php c# c++ html android jquery mysql

Explore

  • 主页
  • 问题
    • 热门问题
    • 最新问题
  • 标签
  • 帮助

Footer

RError.com

关于我们

  • 关于我们
  • 联系我们

Legal Stuff

  • Privacy Policy

帮助

© 2023 RError.com All Rights Reserve   沪ICP备12040472号-5