Swift4.2 Random API 정보

이 기사는 Swift Tweets 2018 Summer에 발표된 LT의 추가 섭렵이다.
아직 변경이 많기 때문에 최신 정보는 swift 창고를 참조하세요.

Swift4.2에서 가져온 Random API 정보


Random API가 필요한 이유


iOS/macOS 개발의 경우 지금까지 사용되고 있음arc4random_uniform.
그러나 크로스플랫폼 개발에서 랜덤 선택 알고리즘 등을 사용하려면 리눅스 환경에 존재하지 않는다arc4random_uniform는 자체 준비가 필요하다.
리눅스 환경으로 설치할 때 하도급은 Upper Bound를 사용하지 않는 함수randomrand 등을 사용할 수 있다.
협조arc4random_uniform를 위해 매개 변수upperBound로 여수를 찾으려고 생각할 수 있지만 그 방법이라면 모듈로 비아스가 생길 수 있다.대부분의 경우 모루비아스는 작아 눈에 띄거나 영향을 주지 않지만, 최악의 경우 원소 간 선택 확률이 2배로 높아진다.그래서 일반적인 실현에서 사용해서는 안 된다.

크로스플랫폼 확인도 번거로우니 표준으로 준비해 주시기 바랍니다!
이런 내용은 원숭이를 홍보하는 모티베이션에 적혀 있다.
https://github.com/apple/swift-evolution/blob/master/proposals/0202-random-unification.md

RandomNumberGenerator


프로토콜은 무작위 생성기의 인터페이스RandomNumberGenerator입니다.
public protocol RandomNumberGenerator {
  // This determines the functionality for producing a random number.
  // Required to implement by all RNGs.
  mutating func next() -> UInt64
}
이외에도 next<T : FixedWidthInteger & UnsignedInteger>() -> Tnext<T : FixedWidthInteger & UnsignedInteger>(upperBound: T) -> T의 기본 설치가 준비되어 있으며, 설치 생성UInt64의 부분만 사용하면 된다.특히 후자upperBound가 덧붙인 함수는Modulo Bias 설치 고려로 안전하게 사용할 수 있다.

Random


그리고 공식적으로 준비한 RNG의 실현은Random(그런데 이름은변경 예정인 것 같다.Random 각 플랫폼은 적당한 청부업자 함수를 사용하는데 이걸로 쓰면 교차 플랫폼에서의 실현이 간단해진다.Random에 따르면 초기화할 필요가 없고 라인이 안전하다는 점은 공통적이지만 cryptographic quality는 청부에 따라 바뀐다고 쓰여 있기 때문에 주의해야 한다(후술).Random는 struct이지만 내부 상태가 없기 때문에 Random.default는 매번 새로운 실례를 되돌려준다.default의 준비는 다음과 같은 쓰기 유형의 실현을 위한 것이다.

종류별 랜덤 생성


static 방법FIxedWidthInteger.random(in: Range<Self>, using: inout RandomNumberGenerator)BinaryFloatingPoint.random(in: Range<Self>, using: inout RandomNumberGenerator) 등이 추가됐다.
수치 이외Collection에도 추가Collection.randomElement(using: inout RandomNumberGenerator)됐다.
생략using한 경우Random.default에는 기본적으로 배후의RandomNumberGenerator API를 무의식적으로 사용했다.
이 디자인이라면 Random() 한 줄로 using 처리를 하지 않기 때문에 준비했습니다Random.default.

총결산


랜덤수 주변arc4random_uniform은 주로 다양한 스타일을 직접 준비하며 랜덤API 구현에 따라 시간을 크게 단축할 수 있다.
Swift4.2부터 랜덤 API를 사용하여 크로스 플랫폼을 쉽게 재활용할 수 있는 프로그램을 작성해 봅시다.

보: Random은 Cryptographically secure입니까?


참고 자료
///While the Random.default generator is automatically seeded and
///thread-safe on every platform, the cryptographic quality of the stream of
///random data produced by the generator may vary. For more detail, see the
///documentation for the APIs used by each platform.
///
///- Apple platforms use arc4random_buf(3) .
///- Linux platforms use getrandom(2) when available; otherwise, they read
/// from /dev/urandom .
또한 랜덤의 댓글.
The aspiration is that this RNG should be cryptographically secure, provide reasonable performance, and should be thread safe. If a vendor is unable to provide these goals, they should document it clearly.
후자의 항목 이름은 Random Number Generator로 매우 번거롭지만 이곳의 대상은 RandomNumberGenerator 프로토콜이 아니라 Random(default RNG)와 그 청부의 실현이다.
현재Random의 세 청부업자는 모두 cryptographically secure인 것 같지만, cryptographically secure가 설치된 플랫폼을 준비할 수 없다는 점을 감안하면Random 자체의 security는 보장할 수 없을 것 같다.
안전성이 요구되는 곳에서cryptographicallysecure를 보장할 수 있는 수단을 사용하는 것이 좋을 것 같다(말하자면 unsecure의 설치를 더하고 그 플랫폼을 사용하는 경우는 많지 않은 것 같다).
그나저나 thread safety에 대한 논평은 단언된 것이고, 프로 레슬링에서는 애매한 것이며, 어느 것이 옳은 것일까?

추가: 부동 소수점 수치에 대한 무작위 생성


우리도 부동점 수치의 생성을 조사하고 이어서 총괄한다.

부동 소수점 값 정보


부동 소수점 수치는 $(기호)*2^{(지수 부분)}*1입니다.(잠정 부분) 달러로 표현1. (잠정 부분) 달러는 2진법1.0..<10.0 (=2)의 범위를 채택한다.
부동점수치1..<2의 범위 내에는 $2^{(임시부분 비트수)}달러의 점만 있고, 이 구간의 지수 부분은 $0$1이기 때문에 임시부분 증가량의 증가량은 점 사이의 간격이고, 더블 상황은 $2^{-52}달러이다.이는 Swift에서 BinaryFloatingPoint.ulpOfOne로 정의됩니다.
그렇다면1.0.nextUp - 1 == .ulpOfOne의 범위도 같은 일을 고려해 보자.지수부는1/2..<1 범위의 지수부-1이고, 다른 한편, 소수부의 자릿수는 변하지 않는다=분수는 변하지 않기 때문에 점의 간격은 $2^{53}$이며, Swift1..<2에 있다.
마찬가지로 .ulpOfOne/21/4..<1/2 범위 내에서 간격이 점점 반으로 바뀌었다.반면1/8..<1/42..<4의 범위 내에서 부동점 수치의 절대치는 작을수록 촘촘하고 크면 클수록 희소하다.

[0,1) 구간 내의 고른 표본 추출


부동 소수점 수치의 생성에 관해서는 포럼에서 인용한 다음과 같은 내용을 참고하시오.
SE-0202 Random Unification#Random Number GeneratorGenerating uniform doubles in the unit interval 항목
즉 생성4..<8하려면 통일 랜덤수Double를 사용하면 된다.r: UInt64Double(r >> 11) * (.ulpOfOne / 2) 구간의 점의 간격이고 아래로 내려가는 구간의 점의 간격은 절반에 불과하기 때문에 (.ulpOfOne / 2) 전체 범위 내에서 이 각도를 사용할 수 있다.1/2..<1는 53비트 랜덤수, 즉 0..<1를 사용하고 상기 Double(r >> 11)는 $2^{-53}로 조합을 통해 0..<2**53의 랜덤수를 생성할 수 있다.
이보다 더 작은 각도를 사용하면(.ulpOfOne / 2)0..<1 반올림이 생겨 최대치.ulpOfOne/4를 생성할 수 있습니다.
// Floatの場合
print(Float(UInt32.max >> 8) * (.ulpOfOne / 2) < 1)   // 2^-23刻み true
print(Float(UInt32.max >> 7) * (.ulpOfOne / 4) < 1)   // 2^-24刻み false

// Doubleの場合
print(Double(UInt64.max >> 11) * (.ulpOfOne / 2) < 1) // 2^-53刻み true
print(Double(UInt64.max >> 10) * (.ulpOfOne / 4) < 1) // 2^-54刻み false
반대로 큰 눈금, 예를 들어1/2..<1을 사용하면 고르게 샘플을 채취할 수 있지만 간격이 커지면 생성할 수 있는 값의 변화가 반으로 바뀐다.
따라서 1.0 구간에서 샘플을 채취하면 .ulpOfOne 눈금이 가장 좋다.0..<1.ulpOfOne/2는 위에서 설명한 방법으로 [0,1)을 생성한 것으로 앞으로 주의하지 않아도 자연스럽게 좋은 실현을 사용할 수 있다.

Swift 구간에서 균일한 샘플링 수행


$구간BinaryFloatingPoint.random(in: range)을 얻을 수 있기 때문에 [low, high)라면 $low,high를 원합니다.
r에서 취한 경우(high - low) * r + low의 최대치1..<2,0..<1의 최대치1.nextDown를 고려했다.1..<2 구간이어서 2.nextDown는 1배로 늘었지만 부동점수의 규격은1..<20..<11.nextDonw * 2 == 2.nextDonw사이였다.
실제 계산하면 결과가 둥글게 1.nextDonw + 1되어 범위에서 벗어난다.
let r = Float(UInt32.max >> 8) * (.ulpOfOne / 2)
print(r == 1)    // false
print(r+1 == 2)  // true
우리는 이러한 계산 오차로 범위를 벗어난 상황을 처리하는 방법을 조사했지만 계산 결과2.nextDown 이상이 되면 취소하는 방법을 발견했다.
스위프트도 이 오류가 있었지만 같은 방법으로 수정http://xoshiro.di.unimi.it/

표준 정적 분포 샘플링


만약에 균등한 분포에서 샘플링을 할 수 있다면 다음과 같은 방법을 통해 표준 정적 분포에서 샘플링을 할 수 있다.
  • https://github.com/apple/swift/pull/17794)。
  • Box-Mulelr transform
  • Marsaglia polar method
  • 이 세 가지에 대해 Swift에 쓴 속도가 비교적 빠른 것은 다음과 같다(Zigguurat의 설치에 자신이 없다...).
    Ziggurat algorithm
    지그재그는 고속이라고 하지만 특별히 최적화되지 않은 상태에서 가장 느리다.나머지 2개는 https://github.com/t-ae/ZigguratSampler/blob/5c1d6d391c34c471f8f23b9e729f689f84ff0cad/Tests/ZigguratSamplerTests/ZigguratSamplerTests.swift#L35-L86가 쓴 것처럼 Box-Muller가 더 빠르다.
    위 단계2의 구현을 균일 분포 생성에 사용하면 Box-Muller보다 polar method로 더 넓은 범위의 값을 생성할 수 있습니다.
    import Foundation
    
    func boxmuller(u1: Double, u2: Double) -> Double {
        return sqrt(-2 * log(u1)) * sin(2 * .pi * u2)
    }
    func polar(u1: Double, u2: Double) -> Double {
        let r = u1*u1 + u2*u2
        assert(0 < r && r < 1)
        return sqrt(-2 * log(r) / r) * u1
    }
    
    // 最大値
    print(boxmuller(u1: .ulpOfOne/2, u2: 0.25))   // 8.571674348652905
    print(polar(u1: (0.5.nextUp)*2-1, u2: 0))     // 12.00727336061225
    
    // 0に近い値
    print(boxmuller(u1: 1.0.nextDown, u2: Double.ulpOfOne/2))   // 1.0394658142353987e-23
    print(polar(u1: (0.5.nextUp)*2-1, u2: 1.0.nextDown*2-1))  // 6.617444900424223e-24
    
    그렇다고 정규 분포로 더 큰 값을 만들 수도 없고, 어떤 방법이 가장 적절한지 모르니 여러분도 공식적으로 준비해주시기 바랍니다.

    좋은 웹페이지 즐겨찾기