加倍数 - 左移与乘法

时间:2010-06-17 07:38:18

标签: c# .net algorithm

之间有什么区别
int size = (int)((length * 200L) / 100L); // (1)

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

(两种情况下长度均为int)

我假设两个代码片段都想要加倍长度参数。

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

请告诉我我错过了什么。

10 个答案:

答案 0 :(得分:38)

<<比乘法更快的想法是推理,就好像.NET jit编译器实际上是一个写得不好的C编译器,写于1970年代。即使它是真的,这个时间点的差异将以皮秒为单位进行测量,即使存在差异,也可能没有。

编写代码以便于阅读。让编译器处理微微优化。基于分析实际场景优化代码,而不是第二次猜测编译器将生成什么。

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

Jill的原创节目:

int x = y * 2;

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

int x = y << 1;

由实习生拉里编辑:哦,我们有一个错误,我们一个人关闭,让我解决这个问题:

int x = y << 1 + 1;
拉里刚刚介绍了一个新的bug。 y * 2 + 1与y&lt;&lt;&lt; 1 + 1;后者实际上是y * 4.

我在真实的实时制作代码中看到了这个错误。精神上很容易进入“转移是乘法”的心态,忘记转移低于优先级而不是添加,而乘法是更高的优先级

我从来没有见过有人通过写x * 2得到算术优先级错误乘以2。人们理解+和*的优先级。很多人都忘记了转移的优先顺序。您实际上没有任何潜在错误的皮秒数?我拒绝。

答案 1 :(得分:26)

这是第三个选项:

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

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

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

答案 2 :(得分:7)

对于普通程序员来说,这更具可读性:

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

除非他们来自强大的C ++位调整背景,否则你的平均程序员会立即知道 第一行所做的事情(它甚至有“2”代表“double”)但必须停下来并暂停第二行。

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

答案 3 :(得分:5)

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

alt text http://www.osnews.com/images/comics/wtfm.jpg

<小时/> 我认为你的代码应该更像这样:

int size = CalculateSizeOfThing(length);

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

    return (length * TotalArraySize) / BytesPerElement;
}

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

答案 4 :(得分:5)

有趣的是,大多数答案都认为编译器会优化乘以2的幂乘以比特移位。很明显,没有一个响应者真的尝试过编译一个bithift而不是乘法来看看编译器实际产生了什么。

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

让我们看看当我们使用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的幂乘以位移。

答案 5 :(得分:2)

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

答案 6 :(得分:2)

当我看到成语时:

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 :(得分:1)

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

另一方面,由于第一次乘法可能会溢出,所以做(x * 200)/ 100与数字加倍是不一样的,所以无法安全地优化这个数字加倍。

答案 8 :(得分:0)

要回答您的实际问题,当length为非负int且代码文本位于unchecked内时,这两个代码段之间似乎没有行为差异} context。

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

如果length为负,则移位操作将产生不正确的结果,而乘法将正常工作。因此,如果允许length变量为负数,则两个代码snippits会有所不同。

(与流行的观点相反,x&lt;&lt; 1与x * 2不同。如果x是无符号整数,它们只是相同的。)

答案 9 :(得分:0)

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

任何编译器都不会将 x * 2 优化为 x << 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.
}

如果我错了,请纠正我。