在我的32位嵌入式C ++应用程序中,我需要执行以下计算:
calc(int milliVolts, int ticks) {
return milliVolts * 32767 * 65536 / 1000 / ticks;
}
现在,由于我平台上的int有32位而milliVolts的范围是[-1000:1000],milliVolts * 32767 * 65536
部分可能导致整数溢出。为了避免这种情况,我将因子65536分成1024,32并重新排序,如下所示:
calc(int milliVolts, int ticks) {
return milliVolts*32767*32/1000*1024/ticks*2;
}
这样,只要编译器保留了乘法和除法的顺序,函数就会正确计算。
Kerninghan和Ritchie在“C编程语言”第2.12节中指出(我没有C ++标准的副本):
C,与大多数语言一样,没有指定运算符操作数的计算顺序。
如果我理解正确的话,编译器可以自由地将我精心挑选的表达式改为不能按预期工作的东西。
如何以保证其功能的方式编写我的功能?
编辑:以下几个答案建议使用浮点计算来避免此问题。这不是一个选项,因为代码在没有浮点运算的CPU上运行。此外,计算是在我的应用程序的硬实时部分,因此使用模拟浮点的速度代价太大。
结论:在Merdad的回答和Matt McNabb的评论的帮助下,我设法找到K& R的相关部分,A7部分,其中说:
运算符的优先级和关联性是完全指定的,但表达式的求值顺序在某些例外情况下是未定义的,即使子表达式涉及副作用。也就是说,除非运算符的定义保证以特定顺序评估其操作数,否则实现可以按任何顺序自由地评估操作数,甚至可以交错评估。但是,每个运算符以与其出现的表达式的解析兼容的方式组合其操作数生成的值。 此规则撤销了以前使用运算具有交换和关联的运算符重新排序表达式的自由,但可能无法进行计算关联。此更改仅影响其精度限制附近的浮点计算,以及可能出现溢出的情况。
所以Merdad是对的:没有什么可担心的。
答案 0 :(得分:33)
实际上:
他们说如果你有f(x) + g(y)
,则无法保证在f(x)
之前评估g(y)
(反之亦然)。
但如果你有
milliVolts * 32767 * 32 / 1000 * 1024 / ticks * 2
它会自动解释为
(((((milliVolts * 32767) * 32) / 1000) * 1024) / ticks) * 2
这意味着不按顺序评估任何操作符的左侧和右侧不会导致任何问题,因为操作符右侧的所有表达式都是变量或数字,在任何一种情况下对右手边的评价是无操作(没有副作用,与函数调用相反)。
因此没有什么可担心的。
答案 1 :(得分:8)
(这是一条评论,但是太长了,无法在评论栏中填写)
“我会使用括号。即使只是为了让代码更清晰。”
是的,是的,还有一个是的。虽然已经证明你的表达式对于编译器来说是明确的,但它仍然可能在随意的读者眼中引起混淆。我知道有些人可以记住所有的优先规则,但我经常发现自己不得不查看这类事情。添加括号将大大有助于向读者说清楚。
但最重要的是,添加评论,准确解释您必须以这种方式执行计算的原因。这正是一种善意的未来维护者进入并试图“简化”代码的情况。一个好的评论将有助于抵御这些不必要的重构。
答案 2 :(得分:1)
我的答案是将milliVolts和ticks投射到int64_t
并执行计算。然后断言结果是否可以存储在int
。
编辑: datahaki建议在计算中使用浮点值。这可能证明是有用的,因为你很可能最终用分数计算。
答案 3 :(得分:1)
你总是会遇到一些圆整错误,我认为不需要解释。所以策略是尽可能地尽量减少误差,考虑到我们不知道毫伏和刻度的值。
我建议将计算分为两个阶段。首先,我们将常量分组: 32767 * 65536/1000 = 2147418.112 = 2147418,误差为0.112,约为20百万分之一。
所以声明一个const int:
const int factor = 2147418;
现在,milivolts处于[-1000,1000]范围内并且在[1,1024]范围内嘀嗒。如果我们计算毫伏/刻度,我们可能会有很大的错误;考虑一下情况:
millivolts=1;
ticks=1024;
intermediate = millivolts/ticks = 0;
result = intermediate * factor = 0; /// Big error, as result should be 2097
所以我建议如下:
int intermediate = factor * millivolt;
int result = intermediate / ticks;
这样,在最坏的情况下(毫伏 = 1000)中间适合32位整数,缺少任何相反的信息,我会假设你可以使用。