【问题标题】:Doubling a number - shift left vs. multiplication加倍数字 - 左移与乘法
【发布时间】:2010-06-17 07:38:17
【问题描述】:

两者有什么区别

int size = (int)((length * 200L) / 100L); // (1)

int size = length << 1; // (2)

(两种情况下长度都是整数)

我假设两个代码 sn-ps 都希望将长度参数加倍。

我很想使用 (2) ... 那么使用 (1) 有什么好处吗?我查看了发生溢出时的边缘情况,两个版本似乎具有相同的行为。

请告诉我我错过了什么。

【问题讨论】:

  • 我不确定,但我认为,现代编译器能够将两个语句优化为(几乎)相同。

标签: c# .net algorithm


【解决方案1】:

&lt;&lt; 比乘法更快的想法是在推理,好像 .NET jit 编译器实际上是 1970 年代编写的优化不佳的 C 编译器。即使这是真的,这个时间点的差异也会以皮秒为单位,即使存在差异,但可能没有。

编写代码,使其易于阅读。让编译器负责 pico 优化。根据分析现实场景优化您的代码,而不是再次猜测编译器将生成什么。

此外,移位运算符与乘法具有相同的语义。例如,考虑以下编辑序列:

Jill 原创节目:

int x = y * 2;

鲍勃编辑:傻吉尔,我会让这个“更快”:

int x = y << 1;

实习生拉里编辑:哦,我们有一个错误,我们差了一个,让我来解决这个问题:

int x = y << 1 + 1;

Larry 刚刚引入了一个新错误。 y * 2 + 1 与 y

我在真实的生产代码中看到了这个错误。很容易在精神上陷入“移位就是乘法”的心态,而忘记了移位优先级低于加法,而乘法优先级高

我从未见过有人通过写 x * 2 将算术优先级错误地乘以 2。人们理解 + 和 * 的优先级。很多人忘记了转移的优先级是什么。您实际上没有保存的皮秒是否值得任何数量的潜在错误?我拒绝。

【讨论】:

  • 我很高兴有人指出了这一点。我喜欢“微微优化”这个词。在我们的语言中,我们称之为“bitneuken”(不能翻译,否则我会被禁止 :-)
  • 我很惊讶这么多人认为x &lt;&lt; 1 的可读性不如x * 2。如果我想乘以 2 的幂,我通常使用 shift,因为它实际上限制了被乘数,因此更好地说明了意图。以及如何为[Flags] 枚举编写值???当然你使用十六进制或移位,而不是否认???
  • @ToxicAvenger:您无法将位图标志与简单的乘法进行比较。如果你想乘以 32,使用 value*32 而不是 value
  • 对。这种说法并不是说位移是合理的,因为它更具可读性,而是它是合理的,因为它更快。我说哪个更快并不重要:当你想要的操作在逻辑上是乘以一个数字然后multiply。当你想要的操作是逻辑上操作位然后操作位。当您将数字视为位数组时,您在错误的抽象级别上进行操作。除非有令人信服的理由这样做,否则不应利用数字被实现为位数组这一事实。
  • +10 表示“当您将数字视为位数组时,您在错误的抽象级别上进行操作。”。 Eric 一如既往地在现场。
【解决方案2】:

这是第三个选项:

int size = length * 2; // Comment explaining what is 2 or what means this multiplication

这一定是最好的选择。因为它可读且易于理解您想要做什么。 至于性能,编译器正在生成相当优化的代码,因此无需担心如此简单的操作。 如果您对溢出有任何疑虑,可以使用checked 块。

编辑 正如许多其他人所提到的,只需在此处使用任何有意义的变量而不是 2

【讨论】:

  • 我认为length * 2' is actually worse than the first option. Your proposal describes the intend of the code less. There is a forth option, which I think is better. He should use named constants instead of 200L`和`100L'。这在描述代码意图方面做得更好。
  • 第四个选项的优势是什么?如果担心溢出检查块就足够了。
  • @Incognito:对不起,我的评论不是很清楚。我不相信性能在这里不是问题,因为编译器会优化。你的选择和我的选择。您对可读性发表评论,我同意可读性非常重要,但不要认为 length * 2 可读性最强。进行这种计算可能是有原因的。因此,我认为使用命名常量对性能更好。顺便提一句。我有点太急于投票给你了。你可以编辑你的评论。这是我撤消此反对票的唯一方法。
  • @Everyone:如果有人不知道length * 2 是什么,那他们的大脑就会受损。关于可读性的这一切都是一堆废话。将任何东西乘以任何整数都很容易阅读:将“事物”乘以“#”因子。您可以通过使用常量使其“更明显易于理解”:const int SCALE_FACTOR = 2; int size = length * SCALE_FACTOR; 但是,如果他们想知道乘以什么,这需要读者弄清楚 SCALE_FACTOR 是什么。不能保证常量的位置是众所周知的。
  • @Everyone:还有第五个选项可以让“不易理解”的代码的意图易于理解,无论是否为常量:它称为注释。
【解决方案3】:

对于普通程序员来说,哪个更易读:

int size = length * 2;
int size = length << 1;

除非他们来自强大的 C++ 位调整背景,否则我敢打赌你的普通程序员立即知道第一行是做什么的(它甚至有数字“2”代表“double”)但必须停下来停在第二行。

事实上,我倾向于评论第二行来解释它的作用,当你可以让代码像第一行那样说话时,这似乎是多余的。

【讨论】:

  • 如果有人需要评论解释移位运算符,在我看来,有人根本不应该编程。
  • 因为你大概有 c/c++ 背景。在我看来,移位运算符的任何使用都是过度优化。我可以把第一条线带到街上的一个人那里,他们可以猜到它的作用。用第二行试试。如果程序员无法在 5 秒内完成它,那么我可能会担心,但这只是不必要的额外思考,没有任何想象的好处。
  • 他真的没有。您可以在没有移位运算符的情况下进行编程,就像在没有三元运算符的情况下进行编程一样容易。你把我的论点变成了一个稻草人——我并不是在暗示我们的目标是让我们的程序对街上的人可读,但是如果有两种方法可以做某事,唯一的区别是清晰,这是有道理的选择更清晰的一个。代码应该传达意图。
  • 归根结底,软件的目标是解决问题,而不是内部美观或超级优化。虽然我同意第二个在理论意义上“更纯粹”,但第一个更容易阅读 - 没有任何意义回到它曾经作为使用更模糊形式的理由的方式这个操作。他们都做同样的事情,我敢打赌编译器知道这一点。使用“独家”版本的操作没有任何意义。
  • 在我看来,程序员的傲慢是软件开发和团队合作的最大障碍之一。
【解决方案4】:

有趣的是,大多数答案都认为编译器会将乘以 2 的幂优化为位移。很明显,没有一个响应者真正尝试过编译位移和乘法来查看编译器实际产生的结果。

这纯粹是一个学术练习;正如几乎每个人都指出的那样,乘法更容易阅读(尽管有人猜测为什么“* 200L / 100L”部分存在 - 这只是为了混淆事物)。同样很明显,在 C# 中用位移位替换乘法不会有任何显着的性能提升,即使在紧密循环中也是如此。如果您需要这种优化,那么您一开始就使用了错误的平台和语言。

让我们看看当我们使用 Visual Studio 2010 中包含的 CSC(C# 编译器)编译一个启用了优化的简单程序时会发生什么。这是第一个程序:

static void Main(string[] args)
{
    int j = 1;

    for (int i = 0; i < 100000; ++i)
    {
        j *= 2;
    }
}

使用 ildasm 反编译生成的可执行文件会为我们提供以下 CIL 清单:

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       23 (0x17)
  .maxstack  2
  .locals init ([0] int32 j,
           [1] int32 i)
  IL_0000:  ldc.i4.1
  IL_0001:  stloc.0
  IL_0002:  ldc.i4.0
  IL_0003:  stloc.1
  IL_0004:  br.s       IL_000e
  IL_0006:  ldloc.0
  IL_0007:  ldc.i4.2
  IL_0008:  mul
  IL_0009:  stloc.0
  IL_000a:  ldloc.1
  IL_000b:  ldc.i4.1
  IL_000c:  add
  IL_000d:  stloc.1
  IL_000e:  ldloc.1
  IL_000f:  ldc.i4     0x186a0
  IL_0014:  blt.s      IL_0006
  IL_0016:  ret
} // end of method Program::Main

这是第二个程序:

static void Main(string[] args)
{
    int j = 1;

    for (int i = 0; i < 100000; ++i)
    {
        j <<= 1;
    }
}

对它进行反编译会得到以下 CIL 清单:

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       23 (0x17)
  .maxstack  2
  .locals init ([0] int32 j,
           [1] int32 i)
  IL_0000:  ldc.i4.1
  IL_0001:  stloc.0
  IL_0002:  ldc.i4.0
  IL_0003:  stloc.1
  IL_0004:  br.s       IL_000e
  IL_0006:  ldloc.0
  IL_0007:  ldc.i4.2
  IL_0008:  shl
  IL_0009:  stloc.0
  IL_000a:  ldloc.1
  IL_000b:  ldc.i4.1
  IL_000c:  add
  IL_000d:  stloc.1
  IL_000e:  ldloc.1
  IL_000f:  ldc.i4     0x186a0
  IL_0014:  blt.s      IL_0006
  IL_0016:  ret
} // end of method Program::Main

注意第 8 行的区别。程序的第一个版本使用乘法 (mul),而第二个版本使用左移 (shl)。

我不确定执行代码时 JIT 对它的作用,但 C# 编译器本身确实将乘以 2 的幂优化为位移位。

【讨论】:

  • 这就是 IL。我认为大部分优化发生在 JIT 中,因此您需要查看 JIT 生成的汇编代码并确保启用所有优化(例如 VS 中的 DEBUG 生成一些操纵行为的属性,您也可以通过 ini 文件操作 JIT 行为)。根据我的经验,在谈论“pico”优化时,查看 IL 几乎没有帮助。
  • C# 编译器不优化的事实并不意味着 JIT 编译器不会。一个体面的 JIT 编译器会优化它生成的代码。毕竟C#编译器生成的只是中间代码。
  • 如果编译 x * 2;和 x
【解决方案5】:

200L100L 代表什么?在我看来,您正在使用magic numbers。您应该尝试以尽可能好的方式描述其意图的方式编写您的程序;使其尽可能可读。对这些值使用命名常量而不是幻数是一种很好的方法。以自己命名的方法提取计算也有帮助。当您这样做时,您会立即看到无法将其重写为x &lt;&lt; 1。不是因为结果会不同,而是因为可维护性会受到影响。当你编写像x &lt;&lt; 1 这样的代码时,下一个程序员不知道这实际上意味着什么,它会增加著名的每分钟WTFs


(来源:osnews.com


我认为您的代码应该看起来更像这样:
int size = CalculateSizeOfThing(length);

private static int CalculateSizeOfThing(int length)
{
    const long TotalArraySize = 200L;
    const long BytesPerElement = 100L;

    return (length * TotalArraySize) / BytesPerElement;
}

当然,这些 const 值的名称是一个疯狂的猜测 :-)

【讨论】:

  • 200L / 100L 不是我自己的代码......我只是偶然发现它并询问了 sematic 差异(我认为可能存在差异,因为 (1) 强制转换长)。这不是选美比赛。所以我猜你的答案有点离题。
【解决方案6】:

当我看到成语时:

int val = (someval * someConstant) / someOtherConstant;

我认为代码的意图是以某种方式进行缩放。它可能是手动定点代码,也可能是避免整数除法的问题。一个具体的例子:

int val = someVal * (4/5); // oops, val = 0 - probably not what was intended

或写为避免来回浮点数:

int val = (int)(someVal * .8); // better than 0 - maybe we wanted to round though - who knows?

当我看到这个成语时:

int val = someVal << someConstant;

我短暂地想知道 someVal 中的各个位是否有更深层次的含义需要移位,然后我开始查看周围的上下文以了解为什么要这样做。

要记住的是,当您编写具有以下内容的代码时:

int val = expr;

有无数种方法可以创建 expr 以使 val 始终具有相同的值。重要的是要考虑你在 expr 中对那些将要关注的人表达的意图。

【讨论】:

    【解决方案7】:

    这听起来像是过早的优化。只做 length * 2 的好处是编译器肯定会优化它并且更容易维护。

    【讨论】:

      【解决方案8】:

      对于任何现代编译器,左移 1 或乘以 2 都会生成相同的代码。它可能是一个将数字添加到自身的添加指令,但这取决于您的目标。

      另一方面,执行 (x*200)/100 与将数字加倍不同,因为第一次乘法可能会溢出,因此无法安全地优化以使数字加倍。

      【讨论】:

        【解决方案9】:

        要回答您的实际问题,当length 是非负数int 并且代码文本位于unchecked 上下文中时,这两个代码片段之间似乎没有行为差异。

        checked 上下文中(int.MaxValue * 200L) 永远不会溢出,因为结果很容易适合long(int)((length * 200L) / 100L 只会在 length * 2 溢出时溢出。在任何情况下,移位运算符都不会溢出。因此,代码片段在 checked 上下文中的行为不同。

        如果length 为负数,则移位运算将产生不正确的结果,而乘法将正常工作。因此,如果允许length 变量为负数,则两个代码片段是不同的。

        (与流行观点相反,x 无符号 整数时它们才相同。)

        【讨论】:

          【解决方案10】:

          无论代码可读性如何:位移位和整数乘法,即使是 2 的常数次方,通常也不相同。

          没有编译器会“优化”x * 2x &lt;&lt; 1,除非它可以向自己证明 x 是一个非负整数。 (如果 x 的类型是unsigned int,那么根据定义当然是正确的。)它还需要知道溢出行为是相同的,或者不会发生溢出。 考虑以下 C# 示例:

          int x = 2000000000, multiplied, shifted;
          // Intention is to store results in 64-bit in case x is large:
          long multA, shiftA, multB, shiftB;
          checked
          {
          shifted = x << 1; // Overflows to negative number.
          multiplied = x * 2; // Throws OverflowException.
          shiftA = x << 1; // Maybe this overflows to negative number too?
          multA = x * 2; // Maybe this throws OverflowException too?
          shiftB = x << 1L; // Happens to double OK, because x is positive.
          multB = x * 2L; // Works regardless of the value of x.
          }
          

          如果我在这里错了,请纠正我。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2016-12-22
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2014-03-31
            • 1970-01-01
            • 1970-01-01
            • 2014-03-22
            相关资源
            最近更新 更多