avr-gcc 计算结果随语句复杂度而变化
avr-gcc calculation results varies with statement complexity
所以我有这行 C 代码,它在 ATmega2560 上获取 10 位 ADC 值并将其缩放以供计数器稍后使用。结果应该在 44 到 4166 之间或类似的东西
uint16_t tcnt = (60 * 16000000)/((100 + adc_latest * 9) * 64 * 36);
我用 avr-gcc 编译并将 tcnt 的值发送到串行进行检查..它超出了预期的范围
在多次检查我的数学和括号后,我想我会简化并重写它如下
uint16_t tcnt = (416667) / (100 + adc_latest *9);
这次我得到了期望值
谁能解释一下这种行为?这是因为涉及到一个非常大的中间值吗?
我猜您正在使用 16 位处理器,并且您已经被整数溢出所困扰。
我猜 adc_latest
是 int
(16 位),或者可能是 uint16_t
。
而常量100、9、64、36都小到可以表示为16位int
。所以子表达式
(100 + adc_latest * 9) * 64 * 36
将全部使用 16 位算法计算。
假设 adc_latest
是 9000。然后 (100 + adc_latest * 9) * 64 * 36
是 20966400 — 但这不是您可以用 16 位表示的数字。所以它会溢出,产生不希望的甚至未定义的结果。
不过,在分子中,常量 16000000 已经太大而无法用 16 位表示,因此编译器会隐式地将其视为 long
整数。
当你重写为
416667 / (100 + adc_latest * 9)
你消除了分母中的溢出,所以一切正常。
如果我的假设是正确的,另一个解决方法是
(60 * 16000000) / ((100 + adc_latest * 9L) * 64 * 36)
9
到 9L
的简单更改将强制使用 long
算术计算分母中的所有内容,消除那里的溢出并且(我预测)导致正确总体结果。
这完全是 C 中表达式“自下而上”求值方式的结果。只有在必须时,子表达式才会提升为更大的通用类型。所以在你原来的表达中,你最终得到了
long / int
那时,分母中的 int
被提升为 long
— 但到那时为时已晚;溢出已经发生。编译器 没有 注意到,由于分母最终将被提升为 long
,因此它还不如将整个分母评估为 long
。它没有将整个分母计算为 long
,因此发生了分母溢出。
附录:我写道,“当你重写它时......你消除了分母中的溢出”,但这是过于简单化了。我举的例子,用 adc_latest = 9000
计算出 81900,这仍然是溢出。因此,重写适用于 adc_latest
的大多数(但不是全部)值,最高可达 7180 左右。所以另一个重写,强制整个分母以 32 位计算,是更可取的。
如果出于某种原因您想避免使用 32 位算术,可能还有其他一些重新排列表达式的方法,以便您可以使用 16 位算术计算整个事物,但我不会尝试这样做马上解决。
所以我有这行 C 代码,它在 ATmega2560 上获取 10 位 ADC 值并将其缩放以供计数器稍后使用。结果应该在 44 到 4166 之间或类似的东西
uint16_t tcnt = (60 * 16000000)/((100 + adc_latest * 9) * 64 * 36);
我用 avr-gcc 编译并将 tcnt 的值发送到串行进行检查..它超出了预期的范围
在多次检查我的数学和括号后,我想我会简化并重写它如下
uint16_t tcnt = (416667) / (100 + adc_latest *9);
这次我得到了期望值
谁能解释一下这种行为?这是因为涉及到一个非常大的中间值吗?
我猜您正在使用 16 位处理器,并且您已经被整数溢出所困扰。
我猜 adc_latest
是 int
(16 位),或者可能是 uint16_t
。
而常量100、9、64、36都小到可以表示为16位int
。所以子表达式
(100 + adc_latest * 9) * 64 * 36
将全部使用 16 位算法计算。
假设 adc_latest
是 9000。然后 (100 + adc_latest * 9) * 64 * 36
是 20966400 — 但这不是您可以用 16 位表示的数字。所以它会溢出,产生不希望的甚至未定义的结果。
不过,在分子中,常量 16000000 已经太大而无法用 16 位表示,因此编译器会隐式地将其视为 long
整数。
当你重写为
416667 / (100 + adc_latest * 9)
你消除了分母中的溢出,所以一切正常。
如果我的假设是正确的,另一个解决方法是
(60 * 16000000) / ((100 + adc_latest * 9L) * 64 * 36)
9
到 9L
的简单更改将强制使用 long
算术计算分母中的所有内容,消除那里的溢出并且(我预测)导致正确总体结果。
这完全是 C 中表达式“自下而上”求值方式的结果。只有在必须时,子表达式才会提升为更大的通用类型。所以在你原来的表达中,你最终得到了
long / int
那时,分母中的 int
被提升为 long
— 但到那时为时已晚;溢出已经发生。编译器 没有 注意到,由于分母最终将被提升为 long
,因此它还不如将整个分母评估为 long
。它没有将整个分母计算为 long
,因此发生了分母溢出。
附录:我写道,“当你重写它时......你消除了分母中的溢出”,但这是过于简单化了。我举的例子,用 adc_latest = 9000
计算出 81900,这仍然是溢出。因此,重写适用于 adc_latest
的大多数(但不是全部)值,最高可达 7180 左右。所以另一个重写,强制整个分母以 32 位计算,是更可取的。
如果出于某种原因您想避免使用 32 位算术,可能还有其他一些重新排列表达式的方法,以便您可以使用 16 位算术计算整个事物,但我不会尝试这样做马上解决。