2010-05-03 82 views
2

當我的Floating-Point Guide昨天是published on slashdot時,我對我的建議comparison function感到非常不滿,這確實是不夠的。所以我終於做了明智的事情,並寫了一個測試套件,看看我能否讓他們全部通過。這是我迄今爲止的結果。我想知道這是否真的像泛型(即不是特定於應用程序)的float比較函數一樣好,還是我仍然錯過了一些邊緣情況。這個函數比較浮點數有什麼問題嗎?

(代碼更新來修復錯誤)

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)); 
    } 

} 
+1

Slashdot上的精英分子可能非常殘酷。 – ChaosPandion 2010-05-03 20:57:56

+0

@Chaos:而且往往是不正確的 – 2010-05-03 21:08:11

+1

我的觀點是,我完全應該嘗試撰寫權威的源代碼,並且爲我建議的「良好」比較函數編寫測試套件過於草率地受到批評。 – 2010-05-03 21:19:24

回答

0

睡在它之後,我意識到,這部分是垃圾:

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

這成爲嚴格的小量變小!一個更明智的版本:

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

儘管如此,if的兩個分支不相互非常一致。當ab比其中一個爲零時要小得多。我真的開始認爲using integer comparison是一個更好的方法。

3

主要的問題,我看到的是你不要讓用戶控制小量。

另外,epsilon的變化取決於被比較的數字的數量級。接近於零的ε很小,接近最大功率ε很大。

我認爲,無論何時您需要談論「足夠接近」等概念時,它都會成爲應用級設計決策。你不能爲此寫一個通用庫。

+0

用戶控制的epsilon被省略以減少測試中的重複。而且比較確實使用了相對誤差,這與ε變化具有相同的效果。你的第三個陳述當然是正確的。 – 2010-05-03 21:17:08