Swift中数据类型精度的一点探索

昨天整理code,顺手写了个UIColor和16进制RGB表示的颜色转换。由于UIColor中的RGBA范围是0..1,所以里面用到了一些乘除法,和强制类型转换:

CGFloat(Float(r)/255.0)
Int(r*255)

写完以后测试了一下貌似没什么问题,就睡觉去了。后来突然想到,1/255并不是一个有理数,而且Int()做的是取地板而不是四舍五入,会不会在这种转换的过程中因为精度的问题造成数据错误呢?于是写了一小段测试代码:

//精度测试,确认转换不会丢失信息
let a  = Array.init(0...255)
let b = a.map{ CGFloat(Float($0)/255.0) }
let c = b.map{ Int($0*255) }
a.elementsEqual(c)

还好结果是True,这个转换过程并没有出错的机会。但我怎么可能就此罢休?通过修改a的初始化条件发现,乘除255的时候,在算257的时候就出错了。按理说1/255并不是一个很小的数字,如果准确的话,16位的存储结构是可以保留足够信息的,所以我尝试这打印了一下各个数据类型的精度:

sizeof(Int)*8//64
sizeof(UInt)*8//64
sizeof(UInt16)*8//16
sizeof(Int32)*8//32

sizeof(Float)*8//32
sizeof(Double)*8//64
sizeof(CGFloat)*8//64

sizeof(Character)*8//72
sizeof(String)*8//192
sizeof(CGRect)*8//256
sizeof(UIColor)*8//64

第一组相当的make sense,Int默认就是64位;第二组是实数部分,令我惊讶的是原来CGFloat也是64位的,而Float反而比它差只有32位。第三组的结果比较值得玩味:Swift里面的String搞得比较复杂,是什么都不奇怪,但我以为Character至少会是8,结果却是72;UIColor本身是由4个CGFloat构成,其size却不是4倍的CGFloat;倒是CGRect比较可以解释,因为它是CGSize和CGPoint组合而成,实质上也是4个CGFloat。

你看到的是非授权版本!爬虫凶猛,请尊重知识产权!

转载请注明出处:http://conanwhf.github.io/2015/12/30/precision/

访问原文「Swift中数据类型精度的一点探索」获取最佳阅读体验并参与讨论


也就是说,sizeof()这个函数,在对某些类型如String等操作的时候,结果是这个结构体所占用的内存空间大小;而对某些类型如Int等,即使它们也是一个结构体(我们已经知道在Swift中所有的类型其实都是结构体),给出的结果却是这个类型所表示的值的内存占用。令人感兴趣的是,在一堆UIXXX的结构体中,为什么独独CGFloat会作为数值类型来看待呢?于是我去翻了翻CGFloat的头文件,发现这家伙真面目是这样的:

public struct CGFloat {
/// The native type used to store the CGFloat, which is Float on
/// 32-bit architectures and Double on 64-bit architectures.
public typealias NativeType = Double
public init()
public init(_ value: Float)
public init(_ value: Double)
/// The native value.
public var native: NativeType
}

真相大白,所以CGFloat在内部实现就是个Double,头文件后面还有很长的各种协议……也就是说,其实CGFloat是可以直接做运算的?试了试,果然没错。那么CGFloat(Float(r)/255.0)这种计算就不妥了,人家一个好好的64位,给生生转成32位了,改成CGFloat(r)/255.0即可。
回到最开始的问题—精度。为了便于测试,我写了个函数:

func precisionTest (div: CGFloat) {
var a : Int = 0
var b : CGFloat
var c : Int
repeat {
a = a + 1
//b = CGFloat(Float(a) / Float(div)) // for Float
b = CGFloat(a) / div    // for CGFloat
c = Int(b * div)
}while c == a
print("div=\(div), a=\(a), b=\(b) @\(b*div), c=\(c) @ \(Double(c)/Double(div))")
}

这个函数会从1开始找,计算在div(除数)固定的情况下,第一个将常数先除再乘后丢失精度的case。之前我们已经知道了在div=255且中间变量使用Float的时候,257/255*255 != 257,但中间变量换成CGFloat之后,程序跑到10000+还没找到错误,被我手动停了👻。另外随机试了几个数字,都是使用CGFloat比Float出错概率要小,这也印证了这两个类型的精度大小区别,只是这种错误的结果随机性比较高,有的用1就已经出错,有的跑了很久也没事,跟div的数值变化没有什么关联性,也就是说这种错误并不全是保存精度大小和Int()截取而造成的。
那么大概是因为除法吧?接下来我固定使用Float(更容易出错便于观察结果),调整不同的div数值来测试,发现:

  1. 1,2,4,8,16,…,512,1024…这些div永远不会出错
  2. 除了2^n作为div,其他数字都会出错(只测试了10000以内的)
  3. 出错的数字都是奇数,但没什么特别明显的规律,跟div的相关性也不大
  4. 有个别出错特别晚的div(超过div本身的数值),他们都可被分解成2^n1+2^n2+2^n3…+2^nm-1

综上所述,在我所测试的这个问题上,结果的误差主要影响还是因为除法。除法实现到最底层(硬件层),终究还是靠电子元器件,也就归结到了二进制计算。所以那些2^n作为除数,只靠移位操作就能解决,没有精度损失;而其他数字则是有一套算法,这也和我们所知的计算机原理相吻合。至于具体怎么算的……如果我搞清楚了也许就能解释3和4了吧……😌(其实我对着打印信息研究了好半天,但数学不行实在研究不出来个结果😭)
以上就是我对Swift中数据类型精度的一些相关探索,如果你想看测试代码,在这里(然而其实真没啥好看)