当前位置: 首页 > 知识库问答 >
问题:

在C#/.NET中哪种代码流模式更有效?

司健柏
2023-03-14

考虑一个方法的主逻辑只应该在给定一定条件下实际运行的情况。据我所知,实现这一点有两种基本方法:

如果逆条件为true,只需返回:

public void aMethod(){
    if(!aBoolean) return;
    // rest of method code goes here
}

如果原始条件为真,则继续执行:

public void aMethod(){
    if(aBoolean){
        // rest of method code goes here
    }
}

共有1个答案

陆正德
2023-03-14

这没什么区别。当前几代处理器(大约是Ivy Bridge和更高版本)不再使用静态分支预测算法,您可以对其进行推理,因此使用其中一种形式不会带来性能上的提高。

在大多数较旧的处理器上,静态分支预测策略通常是假定采用前向条件跳变,而假定不采用后向条件跳变。因此,第一次执行代码时,通过安排最有可能出现的掉线情况,即
if{expected}else{expected},可能会获得较小的性能优势。

但事实是,这种低级的性能分析在用托管的、通过JIT编译的语言(如C#)编写时几乎没有意义。

你得到了很多答案,说可读性和可维护性应该是你在编写代码时的首要考虑。令人遗憾的是,这在“性能”问题中很常见,尽管它是完全真实和无可争辩的,但它大多是绕过问题而不是回答问题。

此外,还不清楚为什么表单“A”在本质上比表单“B”更易读,反之亦然。有同样多的参数--在函数的顶部进行所有参数验证,或者确保只有一个返回点--它最终会按照您的风格指南所说的去做,除非在非常糟糕的情况下,您必须以各种可怕的方式扭曲代码,然后您显然应该做最易读的事情。

除了在概念/理论基础上提出一个完全合理的问题外,理解性能的含义似乎也是一个很好的方法,可以在编写风格指南时做出明智的决定,决定采用哪种通用形式。

现有答案的其余部分都是错误的猜测,或者完全不正确的信息。当然,这有道理。分支预测是复杂的,而且随着处理器变得越来越聪明,理解引擎盖下实际发生(或将要发生)的事情只会变得越来越难。

首先,让我们弄清楚几件事。您在问题中提到了分析未优化代码的性能。不,你不会想这么做的。这是浪费时间;您将获得没有反映实际使用情况的无意义数据,然后尝试从这些数据中得出结论,结果将是错误的(或者可能是正确的,但原因不对,这同样糟糕)。除非您将未优化的代码传送给您的客户机(您不应该这样做),否则您并不关心未优化的代码的执行情况。当用C#编写时,实际上有两个优化级别。第一个是由C#编译器在生成中间语言(IL)时执行的。这是由项目设置中的优化开关控制的。第二级优化是由JIT编译器在将IL翻译成机器代码时执行的。这是一个单独的设置,您实际上可以在启用或禁用优化的情况下分析被JITed的机器代码。当您进行分析或基准测试,甚至分析生成的机器代码时,您需要启用这两个级别的优化。

但是对优化的代码进行基准测试是很困难的,因为优化经常会干扰您要测试的东西。如果您试图对问题中所示的代码进行基准测试,那么优化编译器可能会注意到它们中的任何一个实际上都没有做任何有用的事情,并将它们转换为no-ops。一个no-op的速度和另一个no-op的速度一样快--或者可能并非如此,这实际上更糟,因为你所做的基准测试都是与性能无关的噪音。

最好的方法是在概念层面上理解代码是如何被编译器转换成机器代码的。这不仅使您能够逃避创建良好基准的困难,而且它还具有超越数字的价值。一个优秀的程序员知道如何编写产生正确结果的代码;一个好的程序员知道背后发生了什么(然后做出他们是否需要关心的明智的决定)。

关于编译器是否会将表单“A”和表单“B”转换成等价的代码,已经有了一些猜测。原来答案很复杂。几乎可以肯定,IL是不同的,因为它或多或少是您实际编写的C#代码的直译,而不管是否启用了优化。但事实证明你真的不关心那个,因为IL不是直接执行的。它只在JIT编译器完成之后执行,并且JIT编译器将应用它自己的优化集。具体的优化取决于您编写的代码类型。如果您有:

int A1(bool condition)
{
    if (condition)    return 42;
    return 0;
}

int A2(bool condition)
{
    if (!condition)   return 0;
    return 42;
}

很有可能优化后的机器代码也是一样的。事实上,甚至像这样的东西:

void B1(bool condition)
{
    if (condition)
    {
        DoComplicatedThingA();
        DoComplicatedThingB();
    }
    else
    {
        throw new InvalidArgumentException();
    }
}

void B2(bool condition)
{
    if (!condition)
    {
        throw new InvalidArgumentException();
    }
    DoComplicatedThingA();
    DoComplicatedThingB();
}
C1:
    cmp  condition, 0        // test the value of the bool parameter against 0 (false)
    jne  ConditionWasTrue    // if true (condition != 1), jump elsewhere;
                             //  otherwise, fall through
    call DoComplicatedStuff  // condition was false, so do some stuff
    ret                      // return
ConditionWasTrue:
    call ThrowException      // condition was true, throw an exception and never return
C2:
    cmp  condition, 0        // test the value of the bool parameter against 0 (false)
    je   ConditionWasFalse   // if false (condition == 0), jump elsewhere;
                             //  otherwise, fall through
    call DoComplicatedStuff  // condition was true, so do some stuff
    ret                      // return
ConditionWasFalse:
    call ThrowException      // condition was false, throw an exception and never return

cmp指令相当于您的if测试:它检查condition的值,并确定它是真还是假,在CPU内部隐式设置一些标志。下一个指令是一个条件分支:它基于一个或多个标志的值分支到规范位置/标签。在这种情况下,如果设置了“等于”标志,je将跳转,而如果没有设置“等于”标志,jne将跳转。很简单吧?这正是它在x86系列处理器上的工作方式,它可能是您的JIT编译器为其发射代码的CPU。

现在我们进入了你真正想要问的问题的核心;也就是说,如果比较设置了相等标志,我们是否执行je指令进行跳转,或者如果比较没有设置相等标志,我们是否执行jne指令进行跳转,这有关系吗?同样,不幸的是,答案很复杂,但很有启发性。

在继续之前,我们需要发展对分支预测的一些了解。这些条件跳转是代码中某个任意部分的分支。一个分支可以被接受(这意味着分支实际发生了,处理器开始执行在完全不同的位置找到的代码),也可以不被接受(这意味着执行过程被转移到下一个指令,就好像分支指令根本不在那里一样)。分支预测非常重要,因为错误预测的分支在具有使用推测执行的深度流水线的现代处理器上非常昂贵。如果它预测对了,它就不间断地继续;但是,如果它预测错误,它就必须扔掉它推测地执行的所有代码,重新开始。因此,一种常见的低级优化技术是在分支可能被错误预测的情况下,用聪明的无分支代码替换分支。一个足够聪明的优化器会将if(condition){return42;}else{return0;}转换为一个条件移动,而这个条件移动根本不使用分支,而不管您是以何种方式编写if语句,从而使分支预测变得无关紧要。但我们假设这并没有发生,而您实际上有一个带有条件分支的代码--它是如何被预测的呢?

分支预测如何工作是复杂的,而且随着CPU供应商不断改进其处理器内部的电路和逻辑,变得越来越复杂。改进分支预测逻辑是硬件供应商为其销售的产品增加价值和速度的重要途径,每个供应商都使用不同的专有分支预测机制。更糟的是,每一代处理器使用的分支预测机制略有不同,因此在“一般情况”下进行推理是非常困难的。静态编译器提供了一些选项,允许您优化它们为特定一代微处理器生成的代码,但当将代码传送到大量客户机时,这并不能很好地泛化。您别无选择,只能求助于“通用”优化策略,尽管这通常工作得很好。JIT编译器最大的优点是,因为它在您使用之前就在您的机器上编译代码,所以它可以针对您的特定机器进行优化,就像使用完美选项调用的静态编译器一样。这个promise还没有完全实现,但我不会离题到那个兔子洞。

所有的现代处理器都有动态分支预测,但它们具体如何实现它是可变的。基本上,他们“记住”某一特定的(最近的)分支是否被取走,然后预测下一次它会走这条路。这里有您可以想象到的各种病理情况,相应地,在分支预测逻辑中也有各种有助于减轻可能的损害的情况或方法。不幸的是,在编写代码时,除了完全摆脱分支之外,您自己并不能做任何事情来缓解这个问题,在使用C#或其他托管语言编写代码时,这甚至不是您可以使用的选项。优化器将做它想做的任何事情;你只要交叉手指,希望它是最优化的东西。在我们考虑的代码中,动态分支预测基本上是不相关的,我们将不再讨论它。

重要的是静态分支预测--处理器在第一次执行这个代码,第一次遇到这个分支的时候,当它没有任何真正的基础来做决定的时候,会做什么预测?有一堆看似合理的静态预测算法:

>

  • 预测不采取所有分支(一些早期的处理器实际上使用这种方法)。
  • 假设取“向后”的条件分支,而不取“向前”的条件分支。这里的改进之处在于循环(在执行流中向后跳转)将在大多数时候被正确预测。这是大多数Intel x86处理器使用的静态分支预测策略,直到大约Sandy Bridge。

    由于这种策略使用了很长时间,标准建议是相应地安排if语句:

    if (condition)
    {
        // most likely case
    }
    else
    {
        // least likely case
    }
    

    这看起来可能有悖于直觉,但您必须回到机器代码的样子,这段C#代码将被转换成什么样子。编译器通常将if语句转换为比较,并将条件分支转换为else块。这个静态分支预测算法将预测该分支为“未采取”,因为它是一个前向分支。if块将会在不使用分支的情况下失败,这就是为什么要将“最有可能”的情况放在那里的原因。

    始终使用动态预测器的结果,即使是从未见过的分支。

    这个策略很奇怪,但它实际上是大多数现代英特尔处理器所使用的(大约是常春藤桥和后来的)。基本上,即使动态branch-predictor可能从未看到这个分支,因此可能没有关于它的任何信息,处理器仍然查询它并使用它返回的预测。您可以将其想象为等价于任意静态预测算法。

    在这种情况下,如何安排if语句的条件是完全不重要的,因为初始预测基本上是随机的。大约50%的情况下,你会为错误预测的分支付出代价,而另外50%的情况下,你会从正确预测的分支中获益。而且这只是第一次--在那之后,几率会变得更好,因为动态预测器现在有了更多关于分支性质的信息。

    结果可能是,除了在使用特定静态分支预测策略的处理器上,这并不重要,即使在使用JIT编译的语言(如C#)编写代码时,这也几乎不重要,因为第一次编译延迟超过了单个错误预测分支的成本(可能甚至没有错误预测分支)。

  •  类似资料:
    • 本文向大家介绍ThinkPHP中的URL模式有哪几种?默认是哪种?相关面试题,主要包含被问及ThinkPHP中的URL模式有哪几种?默认是哪种?时的应答技巧和注意事项,需要的朋友参考一下 ThinkPHP支持四种URL模式,可以通过设置URL_MODEL参数来定义,包括普通模式、PATHINFO、REWRITE和兼容模式。 默认模式为:PATHINFO模式,设置URL_MODEL 为1

    • 本文向大家介绍.NET异步编程总结----四种实现模式代码总结,包括了.NET异步编程总结----四种实现模式代码总结的使用技巧和注意事项,需要的朋友参考一下 最近很忙,既要外出找工作又要兼顾老板公司的项目。今天在公司,忙里偷闲,总结一下.NET中的异步调用函数的实现方法,DebugLZQ在写这篇博文之前自己先动手写了本文的所有示例代码,开写之前是做过功课的,用代码说话方有说服力。 本文的内容旨在

    • 问题内容: 一个) b) 答案在此网站上(问题3)。我只是不知道 为什么? 从网站: 3.一个 问题答案: 当您降至最低级别(机器代码,但我将使用汇编语言,因为它主要是一对一映射)时,空循环递减为0和一个递减为50(例如)之间的差异通常沿着的行: 这是因为大多数零碎CPU中的零标志在达到零时由减量指令设置。当增量指令达到50时,通常不能说相同(因为该值没有什么特别之处,不像零)。因此,您需要将寄存

    • 本文向大家介绍谈一谈,java中有哪些代理模式?相关面试题,主要包含被问及谈一谈,java中有哪些代理模式?时的应答技巧和注意事项,需要的朋友参考一下 考察点:代理模式 静态代理,动态代理,Cglib代理。

    • 我在实践中读到了一致性。现在我想了解如何处理InterruptedException 来自书籍的建议: -传播异常(可能在特定于任务的清理之后),使您的方法也成为可中断的阻塞方法;或者,恢复中断状态,以便调用堆栈中更高级别的代码可以处理它 -只有实现线程中断策略的代码才能吞咽中断请求。通用任务和库代码绝不应吞没中断请求。 前两种说法我很清楚,但第三种我不明白。你能澄清一下吗?最好提供示例。 吞下中

    • 我在kurento媒体服务器上使用WebRTC,就我所知WebRTC支持VP8用于视频流,使用opus用于音频流,所以我的问题是,如果我想压缩包括音频和视频的流,那么我需要同时使用(VP8和opus)吗?