在浮子上施加(某些情况下)周期性边界条件的有效方法?

时间:2018-02-18 02:04:28

标签: math graphics floating-point ieee-754

通过简单地执行以下操作,periodic boundary conditions (PBC)的某些情况可以非常有效地强加于整数:

myWrappedWithinPeriodicBoundary = myUIntValue & mask

当边界是半开放范围[0,upperBound]时,这是有效的,其中(不包括)upperBound是2 ^ exp,所以

mask = (1 << exp) - 1

例如:

let pbcUpperBoundExp = 2 // so the periodic boundary will be [0, 4)
let mask = (1 << pbcUpperBoundExp) - 1
for x in -7 ... 7 { print(x & mask, terminator: " ") }

(在Swift中)将打印:

1 2 3 0 1 2 3 0 1 2 3 0 1 2 3

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

2 个答案:

答案 0 :(得分:1)

有几种合理的方法:

  1. fmod(x,1)
  2. modf(x,&dummy) - 具有静态了解其除数的优势,但在我的测试中来自libc.so.6,即使-ffast-math
  3. x-floor(x)(Jens在评论中建议) - 直接支持负面投入
  4. 手动bit-twiddling直接实现
  5. floor
  6. 的手动bit-twiddling实现

    前两个保留了他们输入的标志;如果它是否定的,你可以加1。

    两位操作非常相似:您确定哪些有效位对应于整数部分,并将它们(用于直接实现)或其余(用于实现floor)屏蔽掉。直接实现可以通过浮点除法完成,也可以通过移位来手动重新组装double;即使给定硬件CLZ,前者也要快28%。 floor实现可以立即重建doublefloor永远不会更改其参数的指数,除非它返回0.需要大约20行C。

    以下时间是doublegcc -O3,时序循环超过代表性输入,其中内联操作代码。

    fmod: 41.8 ns
    modf: 19.6 ns
    floor: 10.6 ns
    

    使用-ffast-math:

    fmod: 26.2 ns
    modf: 30.0 ns
    floor: 21.9 ns
    

    位操作:

    direct: 18.0 ns
    floor: 20.6 ns
    

    手动实施具有竞争力,但floor技术是最好的。奇怪的是,三个库函数中的两个在没有-ffast-math的情况下表现更好:即作为PLT函数调用而不是内联内置函数。

答案 1 :(得分:0)

我正在将这个答案添加到我自己的问题中,因为它在撰写本文时描述了我找到的最佳解决方案。它在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
    }
}

在配备2 GHz Intel Core i7的MacBook Pro上, 在随机(有限)Double值上调用wrapped(to range:)的一亿(可能是内联的)需要0.6秒,这大约是每秒1.66亿次调用(不是多线程的)。静态知道或不知道的范围,或者具有2的幂等边界或量度,可以产生一些差异,但不会像人们想象的那样多。

wrappedToUnitRange()需要大约0.2秒,这意味着我的系统每秒有5亿次呼叫。

鉴于正确的情况,wrappedIteratively(to range:)wrappedToUnitRange()一样快。

通过比较基线测试(没有包装一些值,但仍然用它来计算例如一个简单的xor校验和)到一个值被包装的相同测试来进行计时。它们之间的时间差是我给包装调用的时间。

我使用了Swift开发工具链2018-02-21,使用-O -whole-module-optimization -static-stdlib -gnone进行编译。并且已经注意使测试相关,即使用不同分布的真实随机输入来防止死代码移除等。一般地编写包装函数,如BinaryFloatingPoint上的这个扩展,结果被优化为等效代码,就好像我有为浮动和双重编写单独的专用版本。

看到比我更熟练的人(C或Swift或任何其他语言并不重要)会很有趣。

编辑: 对于任何感兴趣的人,这里有一些simd float2的版本:

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

我问了一些可能感兴趣的与simd相关的问题here

EDIT2:

从上面的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

我已更新代码以解释此问题。