加倍数 – 左移与乘法
有什么区别
int size = (int)((length * 200L) / 100L); // (1)
和
int size = length << 1; // (2)
(两种情况下长度均为int)
我假设两个代码片段都想要加倍长度参数。
我很想使用(2)……所以使用(1)有什么好处吗? 我查看溢出发生时的边缘情况,两个版本似乎都有相同的行为。
请告诉我我错过了什么。
这是第三种选择:
int size = length * 2; // Comment explaining what is 2 or what means this multiplication
这一定是最好的选择。 因为它易读且易于理解您想要做什么。 至于性能,编译器正在生成相当优化的代码,因此无需担心这么简单的操作。 如果您对溢出有任何疑虑,可以使用checked
block。
编辑正如许多其他人所提到的,只需使用任何有意义的变量而不是2
。
认为<<
比乘法更快的想法是因为.NET jit编译器实际上是一个在1970年代写的不太优化的C编译器。 即使它是真的,这个时间点的差异将以皮秒为单位,即使存在差异,也可能没有。
编写代码以便于阅读 。 让编译器处理微微优化。 基于剖析现实场景优化代码,而不是第二次猜测编译器将生成什么。
此外,移位运算符不具有与乘法相同的语义。 例如,请考虑以下编辑顺序:
Jill的原创节目:
int x = y * 2;
鲍勃编辑:傻吉尔,我会让这个“更快”:
int x = y << 1;
由实习生拉里编辑:哦,我们有一个错误,我们一个接一个,让我解决这个问题:
int x = y << 1 + 1;
而拉里刚刚推出了一个新的bug。 y * 2 + 1与y << 1 + 1不同; 后者实际上是y * 4。
我在实际的实时生产代码中看到了这个bug 。 精神上很容易进入“转移是乘法”的思维模式,忘记转移的优先级低于加法,而乘法则优先级更高 。
我从来没有见过有人通过写x * 2得到算术优先级错误乘以2。人们理解+和*的优先级。 很多人都忘记了转变的优先顺序。 您实际上没有节省的皮秒是否值得任何数量的潜在错误? 我拒绝。
这对普通程序员来说更具可读性:
int size = length * 2; int size = length << 1;
除非它们来自强大的C ++位调整背景,否则我会打赌你的普通程序员会立即知道第一行是什么(它甚至有“2”代表“double”)但是必须停止并暂停第二行线。
事实上,我倾向于评论第二行,解释它的作用,当你可以让代码像第一行那样进行说话时,这似乎是多余的。
有趣的是,大多数答案都认为编译器会优化乘以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#编译器本身可以certificate没有优化乘以2的幂乘以位移。
200L
和100L
代表什么? 在我看来,你正在使用魔术数字 。 您应该尝试以尽可能好地描述其意图的方式编写您的程序; 使其尽可能可读。 对这些值使用命名常量而不是幻数是一种很好的方法。 同样在其自己的命名方法中提取计算也有帮助。 执行此操作时,您将立即看到无法将其重写为x << 1
。 不是因为结果会有所不同,而是因为可维护性会受到影响。 当您编写类似x << 1
代码时,下一个程序员不知道这实际意味着什么,它会每分钟增加着名的WTF :
alt text http://sofzh.miximages.com/c%23/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
值的名称是一个疯狂的猜测:-)
这听起来像是过早的优化。 只做长度* 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中表达的意图对于将要跟随的人。
对于任何现代编译器,左移1或乘以2将生成相同的代码。 它可能是一个添加指令,将数字添加到自身,但它取决于您的目标。
另一方面,由于第一次乘法可能会溢出,因此执行(x * 200)/ 100与将数字加倍是不同的,因此无法安全地优化以使数字加倍。
为了回答你的实际问题,当这两个代码snippits之间似乎没有行为差异,当length
是非负int
并且代码文本在unchecked
上下文中时。
在已checked
上下文(int.MaxValue * 200L)
中将永远不会溢出,因为结果很容易适合long
(int.MaxValue * 200L)
。 (int)((length * 200L) / 100L
只会在length * 2
溢出时溢出。在任何情况下,移位运算符都不会溢出。因此,代码snippits在checked
上下文中表现不同。
如果length
为负,则移位操作将产生不正确的结果,而乘法将正常工作。 因此,如果允许length
变量为负,则两个代码snippits是不同的。
(与流行的观点相反,x << 1与x * 2不同。如果x是无符号整数,它们只是相同。)