Math 对浮体施加(某些情况下)周期性边界条件的有效方法?

Math 对浮体施加(某些情况下)周期性边界条件的有效方法?,math,graphics,floating-point,ieee-754,Math,Graphics,Floating Point,Ieee 754,通过简单地执行以下操作,可以非常有效地对整数施加某些情况: myWrappedWithinPeriodicBoundary = myUIntValue & mask 当边界为半开范围[0,上限]时,此选项有效,(独占)上限为2^exp,以便 mask = (1 << exp) - 1 问题:是否有任何(大致类似的)有效方法将PBC(某些情况下)强加于浮点数(32或64位IEEE-754)?有几种合理的方法: -具有静态知道其除数的优势,但在我的测试中,它来自libc.s

通过简单地执行以下操作,可以非常有效地对整数施加某些情况:

myWrappedWithinPeriodicBoundary = myUIntValue & mask
当边界为半开范围[0,上限]时,此选项有效,(独占)上限为2^exp,以便

mask = (1 << exp) - 1

问题:是否有任何(大致类似的)有效方法将PBC(某些情况下)强加于浮点数(32或64位IEEE-754)?

有几种合理的方法:

  • -具有静态知道其除数的优势,但在我的测试中,它来自
    libc.so.6
    ,即使使用
    -ffast math
  • (Jens在评论中建议)-直接支持负面输入
  • 手动位旋转直接实现
  • floor的手动位旋转实现
  • 前两个保留其输入的符号;如果为负,则可以添加1

    两位操作非常相似:您可以识别哪些有效位对应于整数部分,并屏蔽它们(用于直接实现)或其他位(用于实现
    floor
    )关闭。直接实现可以使用浮点除法或移位来手动重新组装
    双精度
    ;即使给定硬件,
    地板
    实现也可以立即重建
    双精度
    地板
    从不更改其参数的指数nt,除非它返回0。大约需要20行C

    下面的计时是使用
    double
    gcc-O3
    ,在代表性输入上使用计时循环,操作代码被内联到这些输入上

    fmod: 41.8 ns
    modf: 19.6 ns
    floor: 10.6 ns
    
    使用-ffast数学:

    fmod: 26.2 ns
    modf: 30.0 ns
    floor: 21.9 ns
    
    位操作:

    direct: 18.0 ns
    floor: 20.6 ns
    

    手动实现是有竞争力的,但是
    floor
    技术是最好的。奇怪的是,三个库函数中有两个在没有
    -ffast math
    的情况下性能更好:也就是说,作为PLT函数调用比作为内联内置函数更好。

    我将这个答案添加到我自己的问题中,因为它描述了ing,这是我找到的最好的解决方案。它在Swift 4.1中(应该直接翻译成C),并且已经在各种用例中进行了测试:

    extension BinaryFloatingPoint {
        /// Returns the value after restricting it to the periodic boundary
        /// condition [0, 1).
        /// See https://forums.swift.org/t/why-no-fraction-in-floatingpoint/10337
        @_transparent
        func wrappedToUnitRange() -> Self {
            let fract = self - self.rounded(.down)
            // Have to clamp to just below 1 because very small negative values
            // will otherwise return an out of range result of 1.0.
            // Turns out this:
            if fract >= 1.0 { return Self(1).nextDown } else { return fract }
            // is faster than this:
            //return min(fract, Self(1).nextDown)
        }
        @_transparent
        func wrapped(to range: Range<Self>) -> Self {
            let measure = range.upperBound - range.lowerBound
            let recipMeasure = Self(1) / measure
            let scaled = (self - range.lowerBound) * recipMeasure
            return scaled.wrappedToUnitRange() * measure + range.lowerBound
        }
        @_transparent
        func wrappedIteratively(to range: Range<Self>) -> Self {
            var v = self
            let measure = range.upperBound - range.lowerBound
            while v >= range.upperBound { v = v - measure }
            while v < range.lowerBound { v = v + measure }
            return v
        }
    }
    
    我问了一些可能感兴趣的与simd相关的问题

    编辑2:

    从以上Swift论坛帖子中可以看出:

    // Note that tiny negative values like:
    let x: Float = -1e-08
    // May produce results outside the [0, 1) range:
    let wrapped = x - floor(x)
    print(wrapped < 1.0) // false
    // which may result in out-of-bounds table accesses
    // in common usage, so it's probably better to use:
    let correctlyWrapped = simd_fract(x)
    print(correctlyWrapped < 1.0) // true
    
    //请注意微小的负值,如:
    设x:Float=-1e-08
    //可能产生超出[0,1]范围的结果:
    让包裹=x-地板(x)
    打印(包装<1.0)//错误
    //这可能会导致越界表访问
    //通常情况下,最好使用:
    让正确包装=简单分形(x)
    打印(正确包装<1.0)//true
    

    我已经更新了代码来解释这个问题。

    有趣的问题。如果Swift有
    fmod()的实现
    最好使用它,而不是试图直接旋转位。浮点表示有很多微妙之处。我不认为像尾数部分上的位掩码这样简单的东西就足够了。
    fmod
    与您将要得到的一样好。它需要除法,这通常是一个缓慢的过程说明。结果将是精确的(不会引入新的算术错误)。如果您知道操作数在间隔外少于一个周期,则一个或两个比较和相加或比较和减去序列可能会更快。感谢您提供的大量信息!特别是关于modf(x,&dummy)比fmod(x,1)更快的部分.由于我不知道如何正确解释“97%的速度”(我猜是快了近两倍?),我自己做了一些测试,发现modf(x,&dummy)比fmod(x,1)快6倍左右(我不知道如何正确地转换为速度百分比)。@Jens:97%的速度意味着快了1.97倍(或者运行时间缩短了0.51倍,或者运行时间缩短了49%)。我惊讶于速度提高了6倍:也许你的
    modf
    是内联的?另外,对于单位范围[0,1],我们可以使用(x-floor(x)),根据我的测试,它比modf(x,&dummy)快很多,比simd_fract(x)稍快一些。它的另一个好处是,它可以按原样处理负数,总是返回[0,1]以内的值。@Jens:我同意
    floor
    更快。我觉得自己不考虑它很傻;我想我主要考虑的方法是可以处理范围为2的任意幂的方法(比如整数)。你想让我更新这个答案,还是我们只宣传你的答案?@David:请随时更新这个答案!出于某种原因,在我写下上述评论之前,我实际上也没有考虑过地板。我将用simd float2上的一些信息更新我的答案。
    extension BinaryFloatingPoint {
        /// Returns the value after restricting it to the periodic boundary
        /// condition [0, 1).
        /// See https://forums.swift.org/t/why-no-fraction-in-floatingpoint/10337
        @_transparent
        func wrappedToUnitRange() -> Self {
            let fract = self - self.rounded(.down)
            // Have to clamp to just below 1 because very small negative values
            // will otherwise return an out of range result of 1.0.
            // Turns out this:
            if fract >= 1.0 { return Self(1).nextDown } else { return fract }
            // is faster than this:
            //return min(fract, Self(1).nextDown)
        }
        @_transparent
        func wrapped(to range: Range<Self>) -> Self {
            let measure = range.upperBound - range.lowerBound
            let recipMeasure = Self(1) / measure
            let scaled = (self - range.lowerBound) * recipMeasure
            return scaled.wrappedToUnitRange() * measure + range.lowerBound
        }
        @_transparent
        func wrappedIteratively(to range: Range<Self>) -> Self {
            var v = self
            let measure = range.upperBound - range.lowerBound
            while v >= range.upperBound { v = v - measure }
            while v < range.lowerBound { v = v + measure }
            return v
        }
    }
    
    extension float2 {
        @_transparent
        func wrappedInUnitRange() -> float2 {
            return simd.fract(self)
        }
        @_transparent
        func wrappedToMinusOneToOne() -> float2 {
            let scaled = (self + float2(1, 1)) * float2(0.5, 0.5)
            let scaledFract = scaled - floor(scaled)
            let wrapped = simd_muladd(scaledFract, float2(2, 2), float2(-1, -1))
            // Note that we have to make sure the result is not out of bounds, like
            // simd fract does:
            let oneNextDown = Float(bitPattern:
                0b0_01111110_11111111111111111111111)
            let oneNextDownFloat2 = float2(oneNextDown, oneNextDown)
            return simd.min(wrapped, oneNextDownFloat2)
        }
        @_transparent
        func wrapped(toLowerBound lowerBound: float2,
                     upperBound: float2) -> float2
        {
            let measure = upperBound - lowerBound
            let recipMeasure = simd_precise_recip(measure)
            let scaled = (self - lowerBound) * recipMeasure
            let scaledFract = scaled - floor(scaled)
            // Note that we have to make sure the result is not out of bounds, like
            // simd fract does:
            let wrapped = simd_muladd(scaledFract, measure, lowerBound)
            let maxX = upperBound.x.nextDown // For some reason, this won't be
            let maxY = upperBound.y.nextDown // optimized even when upperBound is
            // statically known, and there is no similar simd function available.
            let maxValue = float2(maxX, maxY)
            return simd.min(wrapped, maxValue)
        }
    }
    
    // Note that tiny negative values like:
    let x: Float = -1e-08
    // May produce results outside the [0, 1) range:
    let wrapped = x - floor(x)
    print(wrapped < 1.0) // false
    // which may result in out-of-bounds table accesses
    // in common usage, so it's probably better to use:
    let correctlyWrapped = simd_fract(x)
    print(correctlyWrapped < 1.0) // true