Skip to content

Commit

Permalink
Merge pull request #40 from orlandos-nl/jo/optimise-double-parsing
Browse files Browse the repository at this point in the history
Optimise double parsing
  • Loading branch information
Joannis authored Feb 26, 2024
2 parents 4f3df5e + 7d50ca1 commit 7937769
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 31 deletions.
26 changes: 9 additions & 17 deletions Sources/IkigaJSON/Codable/JSONDecoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -431,14 +431,11 @@ fileprivate struct KeyedJSONDecodingContainer<Key: CodingKey>: KeyedDecodingCont

func decode(_ type: Float.Type, forKey key: Key) throws -> Float { return try Float(self.decode(Double.self, forKey: key)) }
func decode(_ type: Double.Type, forKey key: Key) throws -> Double {
guard
let (bounds, floating) = floatingBounds(forKey: key),
let double = bounds.makeDouble(from: decoder.pointer, floating: floating)
else {
guard let (bounds, floating) = floatingBounds(forKey: key) else {
throw JSONParserError.decodingError(expected: type, keyPath: codingPath + [key])
}

return double
return bounds.makeDouble(from: decoder.pointer, floating: floating)
}

func decodeInt<F: FixedWidthInteger>(_ type: F.Type, forKey key: Key) throws -> F {
Expand Down Expand Up @@ -502,12 +499,13 @@ fileprivate struct UnkeyedJSONDecodingContainer: UnkeyedDecodingContainer {

var codingPath: [CodingKey] { decoder.codingPath }
var currentIndex = 0
var count: Int?
var count: Int? {
decoder.description.arrayObjectCount()
}
var isAtEnd: Bool { currentIndex >= (count ?? 0) }

init(decoder: _JSONDecoder) {
self.decoder = decoder
self.count = decoder.description.arrayObjectCount()
}

mutating func decodeNil() throws -> Bool {
Expand Down Expand Up @@ -602,14 +600,11 @@ fileprivate struct UnkeyedJSONDecodingContainer: UnkeyedDecodingContainer {
}

mutating func decode(_ type: Double.Type) throws -> Double {
guard
let (bounds, floating) = floatingBounds(),
let double = bounds.makeDouble(from: decoder.pointer, floating: floating)
else {
guard let (bounds, floating) = floatingBounds() else {
throw JSONParserError.decodingError(expected: type, keyPath: codingPath)
}

return double
return bounds.makeDouble(from: decoder.pointer, floating: floating)
}

mutating func decode(_ type: Float.Type) throws -> Float { return Float(try self.decode(Double.self)) }
Expand Down Expand Up @@ -702,14 +697,11 @@ fileprivate struct SingleValueJSONDecodingContainer: SingleValueDecodingContaine
}

func decode(_ type: Double.Type) throws -> Double {
guard
let (bounds, floating) = floatingBounds(),
let double = bounds.makeDouble(from: decoder.pointer, floating: floating)
else {
guard let (bounds, floating) = floatingBounds() else {
throw JSONParserError.decodingError(expected: type, keyPath: codingPath)
}

return double
return bounds.makeDouble(from: decoder.pointer, floating: floating)
}

func decode(_ type: Float.Type) throws -> Float { return try Float(decode(Double.self)) }
Expand Down
122 changes: 109 additions & 13 deletions Sources/IkigaJSON/Core/Bounds.swift
Original file line number Diff line number Diff line change
Expand Up @@ -137,18 +137,18 @@ internal struct Bounds {
/// Uses the fast path for doubles if possible, when failing falls back to Foundation.
///
/// https://www.exploringbinary.com/fast-path-decimal-to-floating-point-conversion/
internal func makeDouble(from pointer: UnsafePointer<UInt8>, floating: Bool) -> Double? {
internal func makeDouble(from pointer: UnsafePointer<UInt8>, floating: Bool) -> Double {
let offset = Int(self.offset)
let length = Int(self.length)
/// Falls back to the foundation implementation which makes too many copies for this use case
///
/// Used when the implementation is unsure
let slice = UnsafeBufferPointer(start: pointer + offset, count: length)
if let string = String(bytes: slice, encoding: .utf8) {
return Double(string)

if floating {
/// Falls back to the foundation implementation which makes too many copies for this use case
///
/// Used when the implementation is unsure
return strtod(pointer + offset, length: length)
} else {
return strtoi(pointer + offset, length: length)
}

return nil
}

internal func makeInt(from pointer: UnsafePointer<UInt8>) -> Int? {
Expand Down Expand Up @@ -176,10 +176,106 @@ fileprivate func decodeUnicode(from data: inout Data, offset: inout Int) throws
}

var unicode: UInt16 = 0
unicode += UInt16(hex0) << 12
unicode += UInt16(hex1) << 8
unicode += UInt16(hex2) << 4
unicode += UInt16(hex3)
unicode &+= UInt16(hex0) &<< 12
unicode &+= UInt16(hex1) &<< 8
unicode &+= UInt16(hex2) &<< 4
unicode &+= UInt16(hex3)

return unicode
}

fileprivate func strtoi(_ pointer: UnsafePointer<UInt8>, length: Int) -> Double {
var pointer = pointer
let endPointer = pointer + length
var notAtEnd: Bool { pointer != endPointer }

var result = 0
while notAtEnd, pointer.pointee.isDigit {
result &*= 10
result &+= numericCast(pointer.pointee &- .zero)
pointer += 1
}
return Double(result)
}

fileprivate func strtod(_ pointer: UnsafePointer<UInt8>, length: Int) -> Double {
var pointer = pointer
let endPointer = pointer + length
var notAtEnd: Bool { pointer != endPointer }

var result: Double
var base = 0
var sign: Double = 1

switch pointer.pointee {
case .minus:
sign = -1
pointer += 1
case .plus:
sign = 1
pointer += 1
default:
()
}

while notAtEnd, pointer.pointee.isDigit {
base &*= 10
base &+= numericCast(pointer.pointee &- .zero)
pointer += 1
}

result = Double(base)

guard notAtEnd else {
return result * sign
}

if pointer.pointee == .fullStop {
pointer += 1

var fraction = 0
var divisor = 1

while notAtEnd, pointer.pointee.isDigit {
fraction &*= 10
fraction &+= numericCast(pointer.pointee &- .zero)
divisor &*= 10
pointer += 1
}

result += Double(fraction) / Double(divisor)

guard notAtEnd else {
return result * sign
}
}

guard pointer.pointee == .e || pointer.pointee == .E else {
return result * sign
}

pointer += 1
var exponent = 0
var exponentSign = 1

switch pointer.pointee {
case .minus:
exponentSign = -1
pointer += 1
case .plus:
exponentSign = 1
pointer += 1
default:
()
}

while notAtEnd, pointer.pointee.isDigit {
exponent &*= 10
exponent &+= numericCast(pointer.pointee &- .zero)
pointer += 1
}
exponent *= exponentSign
result *= pow(10, Double(exponent))

return result * sign
}
5 changes: 5 additions & 0 deletions Sources/IkigaJSON/Core/Pointer+Helpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,9 @@ internal extension UInt8 {
static let squareRight: UInt8 = 0x5d
static let curlyLeft: UInt8 = 0x7b
static let curlyRight: UInt8 = 0x7d

@usableFromInline
var isDigit: Bool {
self >= .zero && self <= .nine
}
}
2 changes: 1 addition & 1 deletion Sources/IkigaJSON/Types/JSONArray.swift
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ public struct JSONArray: ExpressibleByArrayLiteral, Sequence, Equatable {
case .integer:
return description.dataBounds(atIndexOffset: offset).makeInt(from: json)!
case .floatingNumber:
return description.dataBounds(atIndexOffset: offset).makeDouble(from: json, floating: true)!
return description.dataBounds(atIndexOffset: offset).makeDouble(from: json, floating: true)
case .null:
return NSNull()
}
Expand Down

0 comments on commit 7937769

Please sign in to comment.