1.0은 std :: generate_canonical의 유효한 출력입니까?


124

난 항상 임의의 숫자는 0과 1 사이에 거짓말을 생각 하지 않고1 , 그들은 반 개방 구간 [0,1)에서 숫자 즉. cppreference.com에 documentionstd::generate_canonical확인한다이.

그러나 다음 프로그램을 실행할 때 :

#include <iostream>
#include <limits>
#include <random>

int main()
{
    std::mt19937 rng;

    std::seed_seq sequence{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    rng.seed(sequence);
    rng.discard(12 * 629143 + 6);

    float random = std::generate_canonical<float,
                   std::numeric_limits<float>::digits>(rng);

    if (random == 1.0f)
    {
        std::cout << "Bug!\n";
    }

    return 0;
}

다음과 같은 출력을 제공합니다.

Bug!

1, MC 통합에 문제를 일으키는 완벽한을 생성합니다 . 그게 유효한 행동입니까, 아니면 제 편에 오류가 있습니까? 이것은 G ++ 4.7.3과 동일한 출력을 제공합니다.

g++ -std=c++11 test.c && ./a.out

및 clang 3.3

clang++ -stdlib=libc++ -std=c++11 test.c && ./a.out

이것이 올바른 행동이라면 어떻게 피할 수 1있습니까?

편집 1 : git의 G ++는 동일한 문제로 고통받는 것 같습니다. 나는

commit baf369d7a57fb4d0d5897b02549c3517bb8800fd
Date:   Mon Sep 1 08:26:51 2014 +0000

로 컴파일 ~/temp/prefix/bin/c++ -std=c++11 -Wl,-rpath,/home/cschwan/temp/prefix/lib64 test.c && ./a.out하면 동일한 출력이 ldd생성됩니다.

linux-vdso.so.1 (0x00007fff39d0d000)
libstdc++.so.6 => /home/cschwan/temp/prefix/lib64/libstdc++.so.6 (0x00007f123d785000)
libm.so.6 => /lib64/libm.so.6 (0x000000317ea00000)
libgcc_s.so.1 => /home/cschwan/temp/prefix/lib64/libgcc_s.so.1 (0x00007f123d54e000)
libc.so.6 => /lib64/libc.so.6 (0x000000317e600000)
/lib64/ld-linux-x86-64.so.2 (0x000000317e200000)

편집 2 : 여기에 동작을보고했습니다. https://gcc.gnu.org/bugzilla/show_bug.cgi?id=63176

편집 3 : clang 팀이 문제를 알고있는 것 같습니다 : http://llvm.org/bugs/show_bug.cgi?id=18767


21
@David Lively 1.f == 1.f모든 경우에 (모든 경우가 무엇입니까? 어떤 변수도 보지 못했습니다 1.f == 1.f. 여기에는 단 하나의 경우 만 있습니다 : 1.f == 1.f, 그것은 변함없이입니다 true). 이 신화를 더 퍼 뜨리지 마십시오. 부동 소수점 비교는 항상 정확합니다.
R. Martinho Fernandes

15
@DavidLively : 아니요, 그렇지 않습니다. 비교는 항상 정확합니다. 리터럴이 아니라 계산 된 경우 정확하지 않을 수있는 피연산자입니다 .
밝기 경주 궤도에

2
@Galik 1.0 미만의 양수는 유효한 결과입니다. 1.0은 아닙니다. 그렇게 간단합니다. 반올림은 무관합니다. 코드는 임의의 숫자를 가져오고 반올림을 수행하지 않습니다.
R. Martinho Fernandes

7
@DavidLively 그는 1.0과 비교되는 값이 하나 뿐이라고 말합니다. 그 값은 1.0입니다. 1.0에 가까운 값은 1.0과 비교되지 않습니다. 생성 함수의 기능은 중요하지 않습니다. 1.0을 반환하면 1.0과 동일하게 비교됩니다. 1.0을 반환하지 않으면 1.0과 동일하게 비교되지 않습니다. abs(random - 1.f) < numeric_limits<float>::epsilon결과가 1.0에 가까운 지 확인하는 예제 는이 맥락에서 완전히 잘못된 것입니다. 여기에 유효한 결과 인 1.0에 가까운 숫자, 즉 1.0보다 작은 모든 숫자가 있습니다.
R. Martinho Fernandes

4
@Galik 예, 구현하는 데 문제가 있습니다. 그러나 그 문제는 구현자가 처리해야합니다. 사용자는 1.0을 볼 수 없으며 항상 모든 결과의 균등 분포를 확인해야합니다.
R. Martinho Fernandes

답변:


121

문제는 std::mt19937( std::uint_fast32_t) 의 공동 도메인 에서float ; 현재 IEEE754 반올림 모드가 무한대 반올림 모드가 아닌 경우 정밀도 손실이 발생하면 표준에서 설명하는 알고리즘이 잘못된 결과를 제공합니다 (알고리즘 출력 설명과 일치하지 않음). -가장 가까운).

시드를 사용한 mt19937의 7549723 번째 출력은 4294967257 ( 0xffffffd9u)이며, 32 비트 float로 반올림하면 0x1p+32mt19937, 4294967295 ( 0xffffffffu) 의 최대 값과 같습니다 .

이 지정한다면 표준이 올바른 동작을 보장 할 수 URNG 그 행의 출력으로 변환 할 때 RealTypegenerate_canonical라운딩은 음의 무한대쪽으로 수행되는; 이 경우 올바른 결과를 제공합니다. QOI로서 libstdc ++가이 변경을 수행하는 것이 좋습니다.

이 변경으로 1.0더 이상 생성되지 않습니다. 대신 경계 값 0x1.fffffep-N에 대한이 0 < N <= 8(약 더 자주 발생한다 2^(8 - N - 32)N, MT19937의 실제 분포에 따라).

내가 사용하지 않는 것이 좋습니다 floatstd::generate_canonical직접; 오히려 숫자를 생성하고 double음의 무한대로 반올림하십시오.

    double rd = std::generate_canonical<double,
        std::numeric_limits<float>::digits>(rng);
    float rf = rd;
    if (rf > rd) {
      rf = std::nextafter(rf, -std::numeric_limits<float>::infinity());
    }

이 문제는 std::uniform_real_distribution<float>; 솔루션은 동일합니다. 분포를 전문화 double하고 결과를 음의 무한대로 반올림합니다.float .


2
@ 사용자 구현 품질-하나의 준수 구현을 다른 구현보다 더 좋게 만드는 모든 것 (예 : 성능, 엣지 케이스의 동작, 오류 메시지의 유용성).
ecatmur 2014 년

2
@supercat : 조금 벗어나려면 실제로 작은 각도에 대해 사인 함수를 최대한 정확하게 만들려고하는 좋은 이유가 있습니다. 예를 들어 sin (x)의 작은 오류가 sin (x) / x에서 큰 오류로 바뀔 수 있기 때문입니다. x가 0에 가까울 때 실제 계산에서 매우 자주 발생합니다 . π의 배수에 가까운 "초정밀도"는 일반적으로 그 부작용 일뿐입니다.
Ilmari Karonen 2014 년

1
@IlmariKaronen : 충분히 작은 각도의 경우 sin (x)는 단순히 x입니다. Java의 사인 함수와 관련된 내 스 쿼크는 pi의 배수에 가까운 각도입니다. 코드가를 요청할 때 99 %의 sin(x)경우 실제로 원하는 것은 (π / Math.PI) x x의 사인입니다. Java를 유지 관리하는 사람들은 Math.PI의 사인이 π와 Math.PI의 사인이 π와 Math.PI의 차이라고보고하는 느린 수학 루틴을 사용하는 것이 99 %의 응용 프로그램에서도 약간 낮은 값을보고하는 것보다 낫다고 주장합니다. 더 좋을 것입니다 ...
supercat 2014-09-04

3
@ecatmur 제안; 이 게시물을 업데이트하여이 std::uniform_real_distribution<float>결과와 동일한 문제 를 겪고 있음 을 언급 하십시오. (uniform_real_distribution을 검색하는 사람들이이 Q / A가 나오도록합니다.)
MM

1
@ecatmur, 왜 음의 무한대로 반올림 하려는지 잘 모르겠습니다. generate_canonical범위의 숫자를 생성해야 [0,1)하고 가끔 1.0을 생성하는 오류에 대해 이야기하고 있기 때문에 0으로 반올림하는 것이 효과적이지 않습니까?
Marshall Clow

39

표준에 따르면 1.0유효하지 않습니다.

C ++ 11 §26.5.7.2 함수 템플릿 generate_canonical

이 섹션 26.5.7.2에 설명 된 템플릿에서 인스턴스화 된 각 함수는 제공된 균일 난수 생성기의 하나 이상의 호출 결과를 g지정된 RealType의 한 멤버에 매핑합니다. 따라서 생성 된 g ig이 균일하게 분산되면 인스턴스화 결과 t j , 0 ≤ t j <1 은 아래 지정된대로 가능한 한 균일하게 분포됩니다.


25
+1 OP의 프로그램에서 어떤 결함도 볼 수 없으므로 이것을 libstdc ++ 및 libc ++ 버그라고 부르겠습니다.
궤도의 경쾌한 경주

-2

나는에서 비슷한 질문을 만났고 uniform_real_distribution, 주제에 대한 표준의 간결한 표현을 해석하는 방법은 다음과 같습니다.

표준은 항상 측면에서 수학 함수를 정의 수학 (표준 여전히 부동 소수점 그 척 때문에 결코 IEEE 부동 소수점의 측면에서, 수도 있지 평균 IEEE 부동 소수점)입니다. 따라서 표준에서 수학적 표현을 볼 때마다 IEEE가 아닌 실제 수학 에 대한 입니다.

표준은 uniform_real_distribution<T>(0,1)(g)및 둘 다 generate_canonical<T,1000>(g)반 개방 범위 [0,1)의 값을 반환해야 한다고 말합니다 . 그러나 이것들은 수학적 가치입니다. 반 개방 범위 [0,1)에서 실수를 취하여 IEEE 부동 소수점으로 나타낼 때, 반올림되는 시간의 상당 부분이T(1.0) .

경우 T이다 float(24 가수 비트), 우리는 볼 것으로 예상 uniform_real_distribution<float>(0,1)(g) == 1.0f한 25 ^ 2 번에 대해. libc ++에 대한 나의 무차별 대입 실험은 이러한 기대를 확인시켜줍니다.

template<class F>
void test(long long N, const F& get_a_float) {
    int count = 0;
    for (long long i = 0; i < N; ++i) {
        float f = get_a_float();
        if (f == 1.0f) {
            ++count;
        }
    }
    printf("Expected %d '1.0' results; got %d in practice\n", (int)(N >> 25), count);
}

int main() {
    std::mt19937 g(std::random_device{}());
    auto N = (1uLL << 29);
    test(N, [&g]() { return std::uniform_real_distribution<float>(0,1)(g); });
    test(N, [&g]() { return std::generate_canonical<float, 32>(g); });
}

출력 예 :

Expected 16 '1.0' results; got 19 in practice
Expected 16 '1.0' results; got 11 in practice

T입니다 double(53 가수 비트), 우리는 볼 것으로 예상uniform_real_distribution<double>(0,1)(g) == 1.0 한 54 ^ 2 번에 대해. 이 기대치를 시험 할 인내심이 없습니다. :)

내 이해는이 동작이 괜찮다는 것입니다. 그것은 분배는 사실 반환 숫자 캔 "1.0보다 적은"숫자를 반환 주장하는 것을 "반 오픈 rangeness"우리의 감각의 기분을 상하게 할 수있다 동등한1.0; 하지만 이것들은 "1.0"의 두 가지 다른 의미입니다. 첫 번째는 수학적 1.0입니다. 두 번째는 IEEE 단 정밀도 부동 소수점 숫자입니다.1.0 입니다. 그리고 우리는 수십 년 동안 부동 소수점 숫자를 정확히 같은지 비교하지 않도록 배웠습니다.

난수를 제공하는 어떤 알고리즘이든 가끔 정확히 1.0. 이 작업을 수행 할 수 있습니다 아무것도 수학 연산을 제외하고 부동 소수점 숫자로, 그리고 당신이 어떤 수학 연산을 빨리으로, 코드는 반올림 처리해야합니다. 당신이 경우에도 합법적으로 그 가정 generate_canonical<float,1000>(g) != 1.0f, 당신이 여전히 그 가정 할 수 없을 것입니다 generate_canonical<float,1000>(g) + 1.0f != 2.0f때문에 반올림 -. 당신은 그것에서 벗어날 수 없습니다. 그렇다면 왜 우리는이 단일 인스턴스에서 당신이 할 수있는 척할까요?


2
나는이 견해에 강력히 동의하지 않는다. 표준이 반 개방 간격의 값을 지시하고 구현이이 규칙을 위반하면 구현이 잘못된 것입니다. 불행히도 ecatmur가 그의 대답에서 올바르게 지적했듯이 표준은 버그가있는 알고리즘을 지시합니다. 이것은 또한 여기에서 공식적으로 인정됩니다 : open-std.org/jtc1/sc22/wg21/docs/lwg-active.html#2524
cschwan

@cschwan : 내 해석은 구현 규칙을 위반 하지 않는다는 것 입니다. 표준은 [0,1]의 값을 지정합니다. 구현은 [0,1]에서 값을 반환합니다. 이러한 값 중 일부는 IEEE로 반올림 1.0f되지만 IEEE 부동 소수점으로 캐스트 할 때 피할 수 없습니다. 순수한 수학적 결과를 원한다면 기호 계산 시스템을 사용하십시오. IEEE 부동 소수점을 사용 eps하여 1 내에 있는 숫자를 나타내려고하면 죄 상태에있는 것입니다.
Quuxplusone 2017 년

이 버그로 인해 깨질 수있는 가상의 예 : 무언가를 canonical - 1.0f. 모든 표현 가능한 플로트를 들어 [0, 1.0), x-1.0f제로가 아닌. 정확히 1.0f를 사용하면 아주 작은 제수 대신 0으로 나눌 수 있습니다.
Peter Cordes
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.