即使我没有发生舍入错误,比较浮点数的相等性是否也会误导初级开发人员?


31

例如,我想显示一个从0,0.5,... 5开始的按钮列表,每0.5跳一次。我使用for循环来做到这一点,并在按钮STANDARD_LINE上使用了不同的颜色:

var MAX=5.0;
var DIFF=0.5
var STANDARD_LINE=1.5;

for(var i=0;i<=MAX;i=i+DIFF){
    button.text=i+'';
    if(i==STANDARD_LINE){
      button.color='red';
    }
}

在这种情况下,应该没有舍入错误,因为每个值在IEEE 754中都是准确的。

var MAX=10;
var STANDARD_LINE=3;

for(var i=0;i<=MAX;i++){
    button.text=i/2.0+'';
    if(i==STANDARD_LINE/2.0){
      button.color='red';
    }
}

一方面,原始代码更加简单并转发给我。但是我正在考虑一件事:i == STANDARD_LINE是否误导了初级队友?它是否掩盖了浮点数可能具有舍入误差的事实?阅读这篇文章的评论后:

https://stackoverflow.com/questions/33646148/is-hardcode-float-precise-if-it-can-be-represented-by-binary-format-in-ieee-754

似乎有很多开发人员不知道某些浮点数是准确的。即使在我的情况下有效,我是否应该避免浮点数相等性比较?还是我在考虑这个问题?


23
这两个代码清单的行为不相同。3 / 2.0是1.5,但i永远不会是第二个列表中的整数。尝试删除第二个/2.0
candied_orange

27
如果您绝对需要比较两个FP的相等性(因为其他人在它们的很好的答案中指出,这不是必需的,因为您可以只对整数使用循环计数器比较),但是如果您这样做了,那么注释就足够了。就我个人而言,我从事IEEE FP已有很长时间了,如果我看到SPFP直接比较而不带任何评论或任何内容,我仍然会感到困惑。这只是非常精致的代码-至少每次恕我直言都值得评论。

14
无论选择哪种选择,这都是其中一种解释方式和原因的注释绝对必要的情况之一。后来的开发人员甚至可能会不加评论就引起关注。另外,我对button循环中的任何地方都不会改变的事实深为分散注意力。如何访问按钮列表?通过索引到数组还是其他机制?如果是通过对数组的索引访问,则这是另一个支持切换为整数的参数。
jpmc26

9
编写该代码。直到有人认为0.6会是一个更好的步长,然后简单地更改该常数。
tofro

11
“ ...误导初级开发人员”您也会误导高级开发人员。尽管您已对此进行了很多思考,但他们仍会假设您不知道自己在做什么,并且很可能会将其更改为整数版本。
GrandOpener '18

Answers:


116

除非我正在计算的模型需要它们,否则我将始终避免进行连续的浮点运算。浮点算术对于大多数错误而言是不直观的,并且是错误的主要来源。而且,将其引起错误的情况与没有引起错误的情况区分开来,这是一个更加微妙的区别!

因此,使用浮点数作为循环计数器是一个等待发生的缺陷,并且至少需要进行大量背景说明,以说明为什么可以在此处使用0.5,并且这取决于特定的数值。在那时,重写代码以避免浮点计数器可能是更易读的选项。在专业需求层次结构中,可读性紧随其后。


48
我喜欢“等待发生的缺陷”。当然,它现在可能会起作用,但是经过某人的微风会破坏它。
AakashM

10
例如,假设需求发生了变化,那么您将有6个从0到5且等距“标准线”的等距按钮,而不是11个从0到5且等距于“标准线”的等距按钮。按钮。因此,从您那里继承此代码的任何人都将0.5更改为1.0 / 3.0,并将1.5更改为5.0 / 3.0。那会发生什么呢?
大卫·K

8
是的,我对将似乎是一个任意数字(如一个数字可能是“正常”)更改为另一个任意数字(似乎同样是“正常”)的想法感到不舒服,这实际上会带来一个缺陷。
亚历山大-恢复莫妮卡

7
@Alexander:对,您需要有条评论说DIFF must be an exactly-representable double that evenly divides STANDARD_LINE。如果您不想编写该注释(并且依赖所有将来的开发人员对IEEE754 binary64浮点有足够的了解以理解它),则不要以这种方式编写代码。即不要以这种方式编写代码。尤其是因为它可能甚至没有更高的效率:FP加法比整数加法具有更高的延迟,并且它是循环携带的依赖项。而且,编译器(甚至是JIT编译器?)在使用整数计数器进行循环方面可能做得更好。
彼得·科德斯

39

通常,编写循环的方式应考虑做n次。如果您使用的是浮点索引,那么它不再是执行n次操作而是运行直到满足条件的问题。如果这种情况恰好与i<n许多程序员所期望的非常相似,则该代码实际上在做另一件事时似乎在做一件事,而程序员浏览该代码很容易误解。

这有点主观,但是我谦虚地认为,如果您可以重写循环以使用整数索引来循环固定次数,则应该这样做。因此,请考虑以下替代方案:

var DIFF=0.5;                           // pixel increment
var MAX=Math.floor(5.0/DIFF);           // 5.0 is max pixel width
var STANDARD_LINE=Math.floor(1.5/DIFF); // 1.5 is pixel width

for(var i=0;i<=MAX;i++){
    button.text=(i*DIFF)+'';
    if(i==STANDARD_LINE){
      button.color='red';
    }
}

循环以整数表示。在这种情况下,i是整数,并且STANDARD_LINE也被强制为整数。当然,如果碰巧有四舍五入的情况,这当然会更改标准线的位置MAX,因此,您也应该努力防止舍入以获得准确的渲染。但是,您仍然可以更改像素而不是整数的参数,而不必担心浮点数的比较。


3
您可能还需要考虑四舍五入而不是在分配中进行下限,具体取决于您想要的内容。如果应该使该除法得到整数结果,那么当您偶然发现除数恰好略有不同的数字时,下限可能会给您带来惊喜。
ilkkachu,

1
@ilkkachu是的。我的想法是,如果您将5.0设置为最大像素量,那么通过四舍五入,您最好位于5.0的下侧,而不是稍大一点。5.0将是最大。虽然根据您的需要四舍五入可能会更好。无论哪种情况,如果除法仍然创建一个整数,则几乎没有什么区别。
尼尔

4
我非常不同意。停止循环的最佳方法是最自然地表达业务逻辑的条件。如果业务逻辑是您需要11个按钮,则循环应在迭代11处停止。如果业务逻辑是这些按钮相距0.5直到行已满,则循环应在行已满时停止。还有其他考虑因素可以将选择推向一种或另一种机制,但是如果没有这些考虑因素,则选择最符合业务需求的机制。
恢复莫妮卡

你的解释是对Java完全正确/ C ++ /拼音/ Python / ...但JavaScript没有整数,所以iSTANDARD_LINE只看起来像整数。有没有强迫可言,而且DIFFMAXSTANDARD_LINE都只是Number秒。Number用作整数的s在下面应该是安全的2**53,但是它们仍然是浮点数。
埃里克·杜米尼尔

@EricDuminil是的,但这只是一半。另一半是可读性。我确实将其作为这样做的原则原因,而不是为了进行优化。
尼尔,

20

我同意所有其他答案,即使用非整数循环变量通常是不好的样式,即使在这种情况下它也可以正常工作。但是在我看来,这里的风格不好还有另一个原因。

您的代码“知道”可用的线宽从0到5.0恰好是0.5的倍数。应该是?似乎这是一个容易更改的用户界面决定(例如,您可能希望可用宽度之间的差距随着宽度而变大。0.25、0.5、0.75、1.0、1.5、2.0、2.5、3.0、4.0, 5.0之类的东西)。

您的代码“知道”可用的线宽都具有“不错”的表示形式,既是浮点数又是小数。这似乎也可能会改变。(您有时可能需要0.1、0.2、0.3等。)

您的代码“知道”要放在按钮上的文本就是Javascript将这些浮点值转换成的内容。这似乎也可能会改变。(例如,某天您可能希望将宽度设置为1/3,例如您可能不想将其显示为0.33333333333333333等。或者,为了与“ 1.5”保持一致,可能希望看到“ 1.0”而不是“ 1” )

这些对我来说都感觉像是一个单一弱点的表现,这是一种层层融合。这些浮点数是软件内部逻辑的一部分。按钮上显示的文本是用户界面的一部分。它们应该比此处代码中的代码更独立。诸如“以下哪项是应突出显示的默认值”这样的概念?是用户界面问题,它们可能不应该与那些浮点值联系在一起。而且您的循环实际上是(至少应该是)按钮上的循环,而不是线宽上的循环。这样写的话,使用带有非整数值的循环变量的诱惑就消失了:您只需要使用连续的整数或for ... in / for ... of循环。

我的感觉是,大多数情况下,人们可能会试图遍历非整数:这样的原因还有其他原因,与数字问题完全无关,为什么代码应采用不同的组织方式。(并非所有情况;我可以想象某些数学算法可能会以非整数值的循环形式最简洁地表达出来。)


8

一种代码味道是在这样的循环中使用浮点数。

循环可以通过多种方式完成,但是在99.9%的情况下,您应该坚持增加1,否则肯定会造成混乱,不仅是初级开发人员。


我不同意,我认为1的整数倍不会在for循环中造成混淆。我不会认为有代码气味。只有分数。
CodeMonkey

3

是的,您确实想避免这种情况。

浮点数是毫无戒心的程序员的最大陷阱之一(根据我的经验,这意味着几乎每个人)。从依赖浮点相等性测试到将金钱表示为浮点,这都是很大的泥潭。最大的违规行为之一是将一个浮动加到另一个浮动上。关于此类事情的科学文献量很大。

精确地在适当的地方使用浮点数,例如在需要的地方进行实际的数学计算(例如三角函数,绘制函数图等)时,在进行串行操作时要格外小心。平等是正确的。关于IEEE标准中哪一组特定的数字是准确的知识非常神秘,我永远不会依赖它。

在你的情况下,通过墨菲法,从哪里来管理层希望你不要有0.0,0.5,1.0 ......但是0.0,0.4,0.8 ...或任何点; 您会立即感到厌烦,并且初级程序员(或您自己)将花很长时间调试,直到发现问题为止。

在您的特定代码中,我确实会有一个整数循环变量。它代表i第一个按钮,而不是运行编号。

而且,为了更加清晰起见,我可能不会写,i/2但这i*0.5会使发生的事情非常清楚。

var BUTTONS=11;
var STANDARD_LINE=3;

for(var i=0; i<BUTTONS; i++) {
    button.text = (i*0.5)+'';
    if (i==STANDARD_LINE) {
      button.color='red';
    }
}

注意:正如注释中指出的那样,JavaScript实际上没有单独的整数类型。但是可以保证最大15位整数是准确/安全的(请参阅https://www.ecma-international.org/ecma-262/6.0/#sec-number.max_safe_integer),因此对于此类参数(“是否是使用整数或非整数更容易混淆/容易出错”),这很接近于具有单独的类型“ in spirit”;在日常使用中(循环,屏幕坐标,数组索引等),用NumberJavaScript 表示的整数不会让人感到意外。


我将按钮名称更改为其他名称-毕竟有11个按钮,而不是10个。也许FIRST_BUTTON = 0,LAST_BUTTON = 10,STANDARD_LINE_BUTTON =3。除此之外,是的,这就是您应该做的。
gnasher729

的确是@EricDuminil,我在回答中对此做了一些补充。谢谢!
AnoE

1

我认为您的建议都不是很好。相反,我将基于最大值和间距为按钮的数量引入一个变量。然后,就很简单地循环遍历按钮本身的索引。

function precisionRound(number, precision) {
  let factor = Math.pow(10, precision);
  return Math.round(number * factor) / factor;
}

var maxButtonValue = 5.0;
var buttonSpacing = 0.5;

let countEstimate = precisionRound(maxButtonValue / buttonSpacing, 5);
var buttonCount = Math.floor(countEstimate) + 1;

var highlightPosition = 3;
var highlightColor = 'red';

for (let i=0; i < buttonCount; i++) {
    let buttonValue = i / buttonSpacing;
    button.text = buttonValue.toString();
    if (i == highlightPosition) {
        button.color = highlightColor;
    }
}

它可能是更多的代码,但也更具可读性和更强大。


0

您可以通过计算显示的值而不是使用循环计数器作为值来避免整个事情:

var MAX=5.0;
var DIFF=0.5
var STANDARD_LINE=1.5;

for(var i=0; (i*DIFF) < MAX ; i=i+1){
    var val = i * DIFF

    button.text=val+'';

    if(val==STANDARD_LINE){
      button.color='red';
    }
}

-1

浮点运算速度较慢,而整数运算速度较快,因此当我使用浮点运算时,在可以使用整数的地方,我不会不必要地使用它。始终将浮点数(甚至是常数)视为近似值,并且误差很小。在调试过程中用加/减浮点对象替换本机浮点数非常有用,在该对象中,您将每个数字视为一个范围而不是一个点。这样,您就可以在每次算术运算后发现渐进式增长的误差。因此,应将“ 1.5”视为“ 1.45到1.55之间的某个数字”,而将“ 1.50”视为“ 1.495到1.505之间的某个数字”。


5
在为小型微处理器编写C代码时,整数和浮点数之间的性能差异非常重要,但是现代x86派生的CPU如此快地执行浮点运算,以至于使用动态语言的开销很容易抵消任何损失。特别是,JavaScript是否确实在必要时使用NaN有效载荷将每个数字实际上都表示为浮点数?
左右约

1
“浮点运算速度慢而整数运算速度快”是一个历史事实,您不应随着福音的发展而保留它。要补充@leftaroundabout所说的话,惩罚几乎是不相关的并不只是事实,由于自动矢量化编译器和指令集的神奇之处,您可能会发现浮点运算比它们的等效整数运算要快。一个周期内有大量浮子。这个问题无关紧要,但是基本的“整数比浮点数快”的假设在相当长的一段时间内都不成立。
Jeroen Mostert '18年

1
@JeroenMostert SSE / AVX对整数和浮点数都进行了矢量化操作,并且您可以使用较小的整数(因为在指数上不会浪费任何位),因此,原则上,人们仍然经常仍然可以从高度优化的整数代码中获得更高的性能。而不是花车。但是同样,这与大多数应用程序无关,并且绝对与该问题无关。
大约

1
@leftaroundabout:可以。我的意思并不是在任何给定情况下哪个绝对快,只是说“我知道FP速度慢而整数快,所以即使有可能我也会使用整数”并不是一个好动力,甚至在解决这个问题之前您正在做的事情是否需要优化的问题。
Jeroen Mostert '18年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.