Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve [U]Int128 string conversion performance. #65508

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
165 changes: 163 additions & 2 deletions stdlib/public/core/Int128.swift.gyb
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,69 @@ internal struct _${U}Int128 {
internal static var one: Self { Self(high: 0, low: 1) }
}

extension _${U}Int128: CustomStringConvertible {
extension _${U}Int128 {
% if signed:

% else:
/// Divides `x` by rⁿ where r is the `radix`. Returns the quotient,
/// remainder, and digits
private static func div (x: _UInt128, radix: Int) ->
(q:_UInt128, r:UInt64, digits:Int) {
let maxDivisor: UInt64
var digits = _maxPowers[radix-2]
switch radix {
case 2: maxDivisor = UInt64(9_223_372_036_854_775_808)
case 8: maxDivisor = UInt64(9_223_372_036_854_775_808)
case 16: maxDivisor = UInt64(1_152_921_504_606_846_976)
case 10: maxDivisor = UInt64(10_000_000_000_000_000_000)
default:
/// Note: Max radix = 36 so
/// 36¹² = 4_738_381_338_321_616_896 < UInt64.max
var power = radix*radix // squared
power *= power // 4th power
power = power * power * power // 12th power
maxDivisor = UInt64(power); digits = 12
}
let result = x.quotientAndRemainder(dividingBy: _UInt128(high: 0,
low: maxDivisor))
return (result.quotient, result.remainder.low, digits)
}
% end

/// Converts the UInt128 `self` into a string with a given `radix`. The
/// radix string can use uppercase characters if `uppercase` is true.
///
/// Why convert numbers in chunks? This approach reduces the number of
/// calls to division and modulo functions so is more efficient than a naïve
/// digit-based approach. Ideally this code should be in the String module.
/// Further optimizations may be possible by using unchecked string buffers.
func string(withRadix radix:Int = 10, uppercase:Bool = false) -> String {
% if signed:
let str = self.magnitude.string(withRadix:radix, uppercase: uppercase)
if self._isNegative {
return "-" + str
}
% else:
guard 2...36 ~= radix else { return "0" }
if self == Self.zero { return "0" }
var result = (q:self.magnitude, r:UInt64(0), digits:0)
var str = ""
while result.q != Self.zero {
result = Self.div(x: result.q, radix: radix)
var temp = String(result.r, radix: radix, uppercase: uppercase)
if result.q != Self.zero {
temp = String(repeating: "0", count: result.digits-temp.count) + temp
}
str = temp + str
}
% end
return str
}
}

extension _${U}Int128 : CustomStringConvertible {
internal var description: String {
String(self, radix: 10)
string(withRadix: 10)
}
}

Expand All @@ -63,6 +123,107 @@ extension _${U}Int128: CustomDebugStringConvertible {
}
}

extension _${U}Int128 {
public init?<S: StringProtocol>(_ text: S, radix: Int = 10) {
guard !text.isEmpty else { return nil }
guard 2...36 ~= radix else { return nil }
self.init()
if let x = Self.value(from: text, radix: radix) {
self = x
} else {
return nil
}
}

public init?(_ description: String) {
self.init(description, radix:10)
}
}

extension _${U}Int128 {
/// This method breaks `string` into pieces to reduce overhead of the
/// multiplications and allow UInt64 to work in converting larger numbers.
fileprivate static func value<S: StringProtocol>(
from string: S, radix: Int = 10) -> Self? {
// Handles signs and leading zeros
var s = String(string)
% if signed:
var isNegative = false
if s.hasPrefix("-") { isNegative = true; s.removeFirst() }

// Translate the string into a number
guard let r = _UInt128.value(from: s, radix: radix) else { return nil }
% else:
let uradix = UInt64(radix)
if s.hasPrefix("-") { return nil }
if s.hasPrefix("+") { s.removeFirst() }
while s.hasPrefix("0") { s.removeFirst() }

// Translate the string into a number
var r = _UInt128.zero
while !s.isEmpty {
// handle `chunk`-sized digits at a time
let chunk = s.prefix(_UInt128._maxPowers[radix-2])
let size = chunk.count; s.removeFirst(size)
if let uint = UInt64(chunk, radix: radix) {
if size != 0 {
r = _UInt128._multiply(r, timesRadix: uradix, toPower: size)
}
r += _UInt128(high: 0, low: uint)
} else {
return nil
}
}
% end
% if signed:
if isNegative {
return -Self(r)
}
% end
return Self(r)
}
% if signed:

% else:
/// Multiplies `x` by rⁿ where r is the `radix` and returns the product
static func _multiply<T:FixedWidthInteger>(
_ x: T, timesRadix radix: UInt64, toPower n: Int) -> T {
// calculate the powers of the radix and store in a table
func power(of radix:UInt64, to n:Int) -> UInt64 {
var t = radix
var n = n
while n > 0 && t < UInt64.max / radix {
t &*= radix; n -= 1
}
return t
}
let radixToPower: UInt64
if radix == 10 {
radixToPower = Self._powers10[n-1]
} else {
radixToPower = power(of: radix, to: n-1)
}
return x * T(radixToPower)
}

/// Maximum power of the `radix` for an unsigned 64-bit UInt for base
/// indices of 2...36
static let _maxPowers : [Int] = [
63, 40, 31, 27, 24, 22, 21, 20, 19, 18, 17, 17, 16, 16, 15, 15, 15, 15,
14, 14, 14, 14, 13, 13, 13, 13, 13, 13, 13, 13, 13, 12, 12, 12, 12, 12, 12
]

/// Computed powers of 10ⁿ up to UInt64.max
static let _powers10 : [UInt64] = [
10, 100, 1_000, 10_000, 100_000, 1_000_000, 10_000_000, 100_000_000,
1_000_000_000, 10_000_000_000, 100_000_000_000, 1_000_000_000_000,
10_000_000_000_000, 100_000_000_000_000, 1_000_000_000_000_000,
10_000_000_000_000_000, 100_000_000_000_000_000, 1_000_000_000_000_000_000,
10_000_000_000_000_000_000
]
% end
}

extension _${U}Int128: Equatable {
internal static func == (_ lhs: Self, _ rhs: Self) -> Bool {
return (lhs.high, lhs.low) == (rhs.high, rhs.low)
Expand Down