Edited at

Swift で 半精度浮動小数点数 (16 bit Float) を扱う

More than 1 year has passed since last update.


半精度浮動小数点数

Float の値を保存する時など、CGFloat だと現状では 64bit、Float でも 32bit です。 0.0〜1.0 とかのたいした精度を必要としない値を大量に保存したい場合などには、GPU などでは普通に使われている 16-bit 精度の浮動小数点を Swift で使いたいところです。しかし残念ながら、公式 API では Float32 はあっても Float16 は用意されていません。

仕様を調べて、コツコツ実装すればいいかもしれませんが、正直やってられません。

ところが、ふとした事から、Accelerate フレームワークで 変換できる事に気がつきました。そこで、Swift で 独自 Float16 を実装してみました。


Float16.swift

import Foundation

import Accelerate

struct Float16: CustomStringConvertible {

var rawValue: UInt16

static func float_to_float16(value: Float) -> UInt16 {
var input: [Float] = [value]
var output: [UInt16] = [0]
var sourceBuffer = vImage_Buffer(data: &input, height: 1, width: 1, rowBytes: MemoryLayout<Float>.size)
var destinationBuffer = vImage_Buffer(data: &output, height: 1, width: 1, rowBytes: MemoryLayout<UInt16>.size)
vImageConvert_PlanarFtoPlanar16F(&sourceBuffer, &destinationBuffer, 0)
return output[0]
}

static func float16_to_float(value: UInt16) -> Float {
var input: [UInt16] = [value]
var output: [Float] = [0]
var sourceBuffer = vImage_Buffer(data: &input, height: 1, width: 1, rowBytes: MemoryLayout<UInt16>.size)
var destinationBuffer = vImage_Buffer(data: &output, height: 1, width: 1, rowBytes: MemoryLayout<Float>.size)
vImageConvert_Planar16FtoPlanarF(&sourceBuffer, &destinationBuffer, 0)
return output[0]
}

static func floats_to_float16s(values: [Float]) -> [UInt16] {
var inputs = values
var outputs = Array<UInt16>(repeating: 0, count: values.count)
let width = vImagePixelCount(values.count)
var sourceBuffer = vImage_Buffer(data: &inputs, height: 1, width: width, rowBytes: MemoryLayout<Float>.size * values.count)
var destinationBuffer = vImage_Buffer(data: &outputs, height: 1, width: width, rowBytes: MemoryLayout<UInt16>.size * values.count)
vImageConvert_PlanarFtoPlanar16F(&sourceBuffer, &destinationBuffer, 0)
return outputs
}

static func float16s_to_floats(values: [UInt16]) -> [Float] {
var inputs: [UInt16] = values
var outputs: [Float] = Array<Float>(repeating: 0, count: values.count)
let width = vImagePixelCount(values.count)
var sourceBuffer = vImage_Buffer(data: &inputs, height: 1, width: width, rowBytes: MemoryLayout<UInt16>.size * values.count)
var destinationBuffer = vImage_Buffer(data: &outputs, height: 1, width: width, rowBytes: MemoryLayout<Float>.size * values.count)
vImageConvert_Planar16FtoPlanarF(&sourceBuffer, &destinationBuffer, 0)
return outputs
}

init(_ value: Float) {
self.rawValue = Float16.float_to_float16(value: value)
}

var floatValue: Float {
return Float16.float16_to_float(value: self.rawValue)
}

var description: String {
return self.floatValue.description
}

static func + (lhs: Float16, rhs: Float16) -> Float16 {
return Float16(lhs.floatValue + rhs.floatValue)
}

static func - (lhs: Float16, rhs: Float16) -> Float16 {
return Float16(lhs.floatValue - rhs.floatValue)
}

static func * (lhs: Float16, rhs: Float16) -> Float16 {
return Float16(lhs.floatValue * rhs.floatValue)
}

static func / (lhs: Float16, rhs: Float16) -> Float16 {
return Float16(lhs.floatValue / rhs.floatValue)
}
}


Jan 2017: 複数のまとめて変換するコードを追加しました。


実行結果

実行結果は以下のとおりです。やはり精度に問題はありますが、分かって使うには使えそうです。

let a = Float16(0.5) // 0.5

let b = Float16(1000.5) // 1000.5
let c = Float16(1.0 / 32768.0) // 3.05176e-05
let d = Float16(16000.0) // 16000.0
let e = Float16(17000.0) // 16992.0
let f = Float16(20.0 / 0.0) // inf
let g = Float16(0.3) // 0.300049
let h = Float16(Float.pi) // 3.14062


Gist

ソースは Gist からも入手可能になっています。


最後に

Float16 という名前は将来本家に Float16 がやって来たときに(え!ないって?)衝突する可能性があるので、かっこ悪くても、MyFloat16 とかするのがいいかもしれません。

冒頭で、大量に変換する必要がある場合と言いつつ、実は vImageConvert_PlanarFtoPlanar16F()vImageConvert_Planar16FtoPlanarF() は一気にまとめて変換できたりします。なのに、Float16 では一個一個しか変換しないのは、どうなの?と突っ込みが入りそうですが、そこはシレっと無視する事にします。


環境表示

Xcode Version 8.0 (8A218a)

Apple Swift version 3.0 (swiftlang-800.0.46.2 clang-800.0.38)