代码之家  ›  专栏  ›  技术社区  ›  Michael Borgwardt

这个比较浮点数的函数有什么问题吗?

  •  2
  • Michael Borgwardt  · 技术社区  · 15 年前

    当我的 Floating-Point Guide 是昨天 published on slashdot ,我的建议遭到了猛烈抨击 comparison function ,这确实是不够的。所以我终于做了明智的事情,写了一个测试套件,看看我是否能让他们都通过。这是我到目前为止的结果。我想知道这是否真的是一个通用(即不是特定于应用程序的)浮点比较函数所能达到的效果,或者我是否还遗漏了一些边缘情况。

    (更新代码以修复错误)

    import static org.junit.Assert.assertFalse;
    import static org.junit.Assert.assertTrue;
    
    import org.junit.Test;
    
    /**
     * Test suite to demonstrate a good method for comparing floating-point values using an epsilon. Run via JUnit 4.
     *
     * Note: this function attempts a "one size fits all" solution. There may be some edge cases for which it still
     * produces unexpected results, and some of the tests it was developed to pass probably specify behaviour that is
     * not appropriate for some applications. Before using it, make sure it's appropriate for your application!
     *
     * From http://floating-point-gui.de
     *
     * @author Michael Borgwardt
     */
    public class NearlyEqualsTest {
        public static boolean nearlyEqual(float a, float b, float epsilon) {
            final float absA = Math.abs(a);
            final float absB = Math.abs(b);
            final float diff = Math.abs(a - b);
    
            if (a * b == 0) { // a or b or both are zero
                // relative error is not meaningful here
                return diff < (epsilon * epsilon);
            } else { // use relative error
                return diff / (absA + absB) < epsilon;
            }
        }
    
        public static boolean nearlyEqual(float a, float b) {
            return nearlyEqual(a, b, 0.000001f);
        }
    
        /** Regular large numbers - generally not problematic */
        @Test
        public void big() {
            assertTrue(nearlyEqual(1000000f, 1000001f));
            assertTrue(nearlyEqual(1000001f, 1000000f));
            assertFalse(nearlyEqual(10000f, 10001f));
            assertFalse(nearlyEqual(10001f, 10000f));
        }
    
        /** Negative large numbers */
        @Test
        public void bigNeg() {
            assertTrue(nearlyEqual(-1000000f, -1000001f));
            assertTrue(nearlyEqual(-1000001f, -1000000f));
            assertFalse(nearlyEqual(-10000f, -10001f));
            assertFalse(nearlyEqual(-10001f, -10000f));
        }
    
        /** Numbers around 1 */
        @Test
        public void mid() {
            assertTrue(nearlyEqual(1.0000001f, 1.0000002f));
            assertTrue(nearlyEqual(1.0000002f, 1.0000001f));
            assertFalse(nearlyEqual(1.0002f, 1.0001f));
            assertFalse(nearlyEqual(1.0001f, 1.0002f));
        }
    
        /** Numbers around -1 */
        @Test
        public void midNeg() {
            assertTrue(nearlyEqual(-1.000001f, -1.000002f));
            assertTrue(nearlyEqual(-1.000002f, -1.000001f));
            assertFalse(nearlyEqual(-1.0001f, -1.0002f));
            assertFalse(nearlyEqual(-1.0002f, -1.0001f));
        }
    
        /** Numbers between 1 and 0 */
        @Test
        public void small() {
            assertTrue(nearlyEqual(0.000000001000001f, 0.000000001000002f));
            assertTrue(nearlyEqual(0.000000001000002f, 0.000000001000001f));
            assertFalse(nearlyEqual(0.000000000001002f, 0.000000000001001f));
            assertFalse(nearlyEqual(0.000000000001001f, 0.000000000001002f));
        }
    
        /** Numbers between -1 and 0 */
        @Test
        public void smallNeg() {
            assertTrue(nearlyEqual(-0.000000001000001f, -0.000000001000002f));
            assertTrue(nearlyEqual(-0.000000001000002f, -0.000000001000001f));
            assertFalse(nearlyEqual(-0.000000000001002f, -0.000000000001001f));
            assertFalse(nearlyEqual(-0.000000000001001f, -0.000000000001002f));
        }
    
        /** Comparisons involving zero */
        @Test
        public void zero() {
            assertTrue(nearlyEqual(0.0f, 0.0f));
            assertTrue(nearlyEqual(0.0f, -0.0f));
            assertTrue(nearlyEqual(-0.0f, -0.0f));
            assertFalse(nearlyEqual(0.00000001f, 0.0f));
            assertFalse(nearlyEqual(0.0f, 0.00000001f));
            assertFalse(nearlyEqual(-0.00000001f, 0.0f));
            assertFalse(nearlyEqual(0.0f, -0.00000001f));
    
            assertTrue(nearlyEqual(0.0f, 0.00000001f, 0.01f));
            assertTrue(nearlyEqual(0.00000001f, 0.0f, 0.01f));
            assertFalse(nearlyEqual(0.00000001f, 0.0f, 0.000001f));
            assertFalse(nearlyEqual(0.0f, 0.00000001f, 0.000001f));
    
            assertTrue(nearlyEqual(0.0f, -0.00000001f, 0.1f));
            assertTrue(nearlyEqual(-0.00000001f, 0.0f, 0.1f));
            assertFalse(nearlyEqual(-0.00000001f, 0.0f, 0.00000001f));
            assertFalse(nearlyEqual(0.0f, -0.00000001f, 0.00000001f));
        }
    
        /** Comparisons of numbers on opposite sides of 0 */
        @Test
        public void opposite() {
            assertFalse(nearlyEqual(1.000000001f, -1.0f));
            assertFalse(nearlyEqual(-1.0f, 1.000000001f));
            assertFalse(nearlyEqual(-1.000000001f, 1.0f));
            assertFalse(nearlyEqual(1.0f, -1.000000001f));
            assertTrue(nearlyEqual(1e10f * Float.MIN_VALUE, -1e10f * Float.MIN_VALUE));
        }
    
        /**
         * The really tricky part - comparisons of numbers very close to zero.
         */
        @Test
        public void ulp() {
            assertTrue(nearlyEqual(Float.MIN_VALUE, -Float.MIN_VALUE));
            assertTrue(nearlyEqual(-Float.MIN_VALUE, Float.MIN_VALUE));
            assertTrue(nearlyEqual(Float.MIN_VALUE, 0));
            assertTrue(nearlyEqual(0, Float.MIN_VALUE));
            assertTrue(nearlyEqual(-Float.MIN_VALUE, 0));
            assertTrue(nearlyEqual(0, -Float.MIN_VALUE));
    
            assertFalse(nearlyEqual(0.000000001f, -Float.MIN_VALUE));
            assertFalse(nearlyEqual(0.000000001f, Float.MIN_VALUE));
            assertFalse(nearlyEqual(Float.MIN_VALUE, 0.000000001f));
            assertFalse(nearlyEqual(-Float.MIN_VALUE, 0.000000001f));
    
            assertFalse(nearlyEqual(1e25f * Float.MIN_VALUE, 0.0f, 1e-12f));
            assertFalse(nearlyEqual(0.0f, 1e25f * Float.MIN_VALUE, 1e-12f));
            assertFalse(nearlyEqual(1e25f * Float.MIN_VALUE, -1e25f * Float.MIN_VALUE, 1e-12f));
    
            assertTrue(nearlyEqual(1e25f * Float.MIN_VALUE, 0.0f, 1e-5f));
            assertTrue(nearlyEqual(0.0f, 1e25f * Float.MIN_VALUE, 1e-5f));
            assertTrue(nearlyEqual(1e20f * Float.MIN_VALUE, -1e20f * Float.MIN_VALUE, 1e-5f));
        }
    
    }
    
    2 回复  |  直到 15 年前
        1
  •  3
  •   Pyrolistical    15 年前

    我看到的主要问题是你不允许用户控制epsilon。

    同样,epsilon的变化取决于被比较的数字的数量级。接近零的epsilon很小,接近最大功率的epsilon很大。

    我认为每当你需要讨论“足够接近”这样的概念时,它就变成了一个应用程序级的设计决策。不能为此编写通用库。

        2
  •  0
  •   Michael Borgwardt    15 年前

    经过仔细研究,我意识到这部分是垃圾:

        if (a*b==0) {
            return diff < Float.MIN_VALUE / epsilon;
    

    这就变成 较少的 就像epsilon变小一样严格!一个更合理的版本:

        if (a * b == 0) {
            return diff < (epsilon * epsilon);
    

    尽管如此, if 彼此不太一致。当 a b 比其中一个为零时要小。我真的开始觉得 using integer comparison 是一个更好的方法。

    推荐文章