|
12 | 12 |
|
13 | 13 | import TestsUtils
|
14 | 14 |
|
15 |
| -// |
16 | 15 | // Mini benchmark implementing roman numeral conversions to/from integers.
|
17 |
| -// Measures performance of Substring.starts(with:) and String.append(), |
18 |
| -// with very short string arguments. |
| 16 | +// Measures performance of String/Substring/UTF8View with very short string |
| 17 | +// arguments and methods: hasPrefix/starts(with:), removeFirst/dropFirst and |
| 18 | +// String.append(). |
19 | 19 | //
|
| 20 | +// For comparison, there's one extra variant with character based parsing |
| 21 | +// algorithm: `Roman.DictCharInt.map.reduce`. |
| 22 | + |
| 23 | +let t: [BenchmarkCategory] = [.api, .String, .algorithm] |
| 24 | +let N = 270 // 1100 |
20 | 25 |
|
21 | 26 | public let RomanNumbers = [
|
| 27 | + // Imperative style variants: |
| 28 | + // String permuatations |
| 29 | + BenchmarkInfo( |
| 30 | + name: "Roman.String.hasPrefix.removeFirst", |
| 31 | + runFunction: { |
| 32 | + checkId($0, upTo: N, { $0.romanNumeral }, Int.init(romanShPrF:)) }, |
| 33 | + tags: t), |
| 34 | + BenchmarkInfo( |
| 35 | + name: "Roman.String.hasPrefix.dropFirst", |
| 36 | + runFunction: { |
| 37 | + checkId($0, upTo: N, { $0.romanNumeral }, Int.init(romanShPdF:)) }, |
| 38 | + tags: t), |
| 39 | + BenchmarkInfo( |
| 40 | + name: "Roman.String.startsWith.dropFirst", |
| 41 | + runFunction: { |
| 42 | + checkId($0, upTo: N, { $0.romanNumeral }, Int.init(romanSsWdF:)) }, |
| 43 | + tags: t), |
| 44 | + BenchmarkInfo( |
| 45 | + name: "Roman.String.startsWith.removeFirst", |
| 46 | + runFunction: { |
| 47 | + checkId($0, upTo: N, { $0.romanNumeral }, Int.init(romanSsWrF:)) }, |
| 48 | + tags: t), |
| 49 | + // Substring permutations |
| 50 | + BenchmarkInfo( |
| 51 | + name: "Roman.Substring.hasPrefix.removeFirst", |
| 52 | + runFunction: { |
| 53 | + checkId($0, upTo: N, { $0.romanNumeral }, Int.init(romanSShPrF:)) }, |
| 54 | + tags: t), |
| 55 | + BenchmarkInfo( |
| 56 | + name: "Roman.Substring.hasPrefix.dropFirst", |
| 57 | + runFunction: { |
| 58 | + checkId($0, upTo: N, { $0.romanNumeral }, Int.init(romanSShPdF:)) }, |
| 59 | + tags: t), |
| 60 | + BenchmarkInfo( |
| 61 | + name: "Roman.Substring.startsWith.dropFirst", |
| 62 | + runFunction: { |
| 63 | + checkId($0, upTo: N, { $0.romanNumeral }, Int.init(romanSSsWdF:)) }, |
| 64 | + tags: t), |
| 65 | + BenchmarkInfo( |
| 66 | + name: "Roman.Substring.startsWith.removeFirst", |
| 67 | + runFunction: { |
| 68 | + checkId($0, upTo: N, { $0.romanNumeral }, Int.init(romanSSsWrF:)) }, |
| 69 | + tags: t), |
| 70 | + // UTF8View SubSequence |
| 71 | + BenchmarkInfo( |
| 72 | + name: "Roman.UTF8ViewSS.startsWith.dropFirst", |
| 73 | + runFunction: { |
| 74 | + checkId($0, upTo: N, { $0.romanNumeral }, Int.init(romanU8SSsWdF:)) }, |
| 75 | + tags: t), |
| 76 | + BenchmarkInfo( |
| 77 | + name: "Roman.UTF8ViewSS.startsWith.removeFirst", |
| 78 | + runFunction: { |
| 79 | + checkId($0, upTo: N, { $0.romanNumeral }, Int.init(romanU8SSsWrF:)) }, |
| 80 | + tags: t), |
| 81 | + // FP-style variants: |
22 | 82 | BenchmarkInfo(
|
23 |
| - name: "RomanNumbers", |
24 |
| - runFunction: run_RomanNumbers, |
25 |
| - tags: [.api, .String, .algorithm]) |
| 83 | + name: "Roman.String.hasPrefix.dropFirst.RI", |
| 84 | + runFunction: { |
| 85 | + checkId($0, upTo: N, { $0.romanReduceInto }, Int.init(romanReduceInto:))}, |
| 86 | + tags: t), |
| 87 | + BenchmarkInfo( |
| 88 | + name: "Roman.String.hasPrefix.dropFirst.R", |
| 89 | + runFunction: { |
| 90 | + checkId($0, upTo: N, { $0.romanReduce }, Int.init(romanReduce:)) }, |
| 91 | + tags: t), |
| 92 | + BenchmarkInfo( |
| 93 | + name: "Roman.DictCharInt.map.reduce", |
| 94 | + runFunction: { |
| 95 | + checkId($0, upTo: N, { $0.romanNumeral }, Int.init(romanMapReduce:)) }, |
| 96 | + tags: t), |
26 | 97 | ]
|
27 | 98 |
|
| 99 | +@inline(__always) |
| 100 | +func checkId(_ n: Int, upTo limit: Int, _ itor: (Int) -> String, |
| 101 | + _ rtoi: (String) -> Int?) { |
| 102 | + for _ in 1...n { |
| 103 | + CheckResults( |
| 104 | + zip(1...limit, (1...limit).map(itor).map(rtoi)).allSatisfy { $0 == $1 }) |
| 105 | + } |
| 106 | +} |
| 107 | + |
28 | 108 | let romanTable: KeyValuePairs<String, Int> = [
|
29 |
| - "M": 1000, |
30 |
| - "CM": 900, |
31 |
| - "D": 500, |
32 |
| - "CD": 400, |
33 |
| - "C": 100, |
34 |
| - "XC": 90, |
35 |
| - "L": 50, |
36 |
| - "XL": 40, |
37 |
| - "X": 10, |
38 |
| - "IX": 9, |
39 |
| - "V": 5, |
40 |
| - "IV": 4, |
| 109 | + "M": 1000, "CM": 900, "D": 500, "CD": 400, |
| 110 | + "C": 100_, "XC": 90_, "L": 50_, "XL": 40_, |
| 111 | + "X": 10__, "IX": 9__, "V": 5__, "IV": 4__, |
41 | 112 | "I": 1,
|
42 | 113 | ]
|
43 | 114 |
|
| 115 | +let romanTableUTF8 = romanTable.map { ($0.utf8, $1) } |
| 116 | + |
44 | 117 | extension BinaryInteger {
|
| 118 | + // Imperative Style |
| 119 | + // See https://www.rosettacode.org/wiki/Roman_numerals/Encode#Swift |
| 120 | + // See https://www.rosettacode.org/wiki/Roman_numerals/Decode#Swift |
| 121 | + |
45 | 122 | var romanNumeral: String {
|
46 | 123 | var result = ""
|
47 |
| - var value = self |
48 |
| - outer: |
49 |
| - while value > 0 { |
50 |
| - var position = 0 |
51 |
| - for (i, (key: s, value: v)) in romanTable[position...].enumerated() { |
52 |
| - if value >= v { |
53 |
| - result += s |
54 |
| - value -= Self(v) |
55 |
| - position = i |
56 |
| - continue outer |
57 |
| - } |
| 124 | + var n = self |
| 125 | + for (numeral, value) in romanTable { |
| 126 | + while n >= value { |
| 127 | + result += numeral |
| 128 | + n -= Self(value) |
58 | 129 | }
|
59 |
| - fatalError("Unreachable") |
60 | 130 | }
|
61 | 131 | return result
|
62 | 132 | }
|
63 | 133 |
|
64 |
| - init?(romanNumeral: String) { |
| 134 | + // String permutations (romanS) |
| 135 | + |
| 136 | + init?(romanShPrF number: String) { |
65 | 137 | self = 0
|
66 |
| - var input = Substring(romanNumeral) |
67 |
| - outer: |
68 |
| - while !input.isEmpty { |
69 |
| - var position = 0 |
70 |
| - for (i, (key: s, value: v)) in romanTable[position...].enumerated() { |
71 |
| - if input.starts(with: s) { |
72 |
| - self += Self(v) |
73 |
| - input = input.dropFirst(s.count) |
74 |
| - position = i |
75 |
| - continue outer |
76 |
| - } |
| 138 | + var raw = number |
| 139 | + for (numeral, value) in romanTable { |
| 140 | + while raw.hasPrefix(numeral) { |
| 141 | + self += Self(value) |
| 142 | + raw.removeFirst(numeral.count) |
| 143 | + } |
| 144 | + } |
| 145 | + guard raw.isEmpty else { return nil } |
| 146 | + } |
| 147 | + |
| 148 | + init?(romanShPdF number: String) { |
| 149 | + self = 0 |
| 150 | + var raw = number |
| 151 | + for (numeral, value) in romanTable { |
| 152 | + while raw.hasPrefix(numeral) { |
| 153 | + self += Self(value) |
| 154 | + raw = String(raw.dropFirst(numeral.count)) |
| 155 | + } |
| 156 | + } |
| 157 | + guard raw.isEmpty else { return nil } |
| 158 | + } |
| 159 | + |
| 160 | + init?(romanSsWdF number: String) { |
| 161 | + self = 0 |
| 162 | + var raw = number |
| 163 | + for (numeral, value) in romanTable { |
| 164 | + while raw.starts(with: numeral) { |
| 165 | + self += Self(value) |
| 166 | + raw = String(raw.dropFirst(numeral.count)) |
| 167 | + } |
| 168 | + } |
| 169 | + guard raw.isEmpty else { return nil } |
| 170 | + } |
| 171 | + |
| 172 | + init?(romanSsWrF number: String) { |
| 173 | + self = 0 |
| 174 | + var raw = number |
| 175 | + for (numeral, value) in romanTable { |
| 176 | + while raw.starts(with: numeral) { |
| 177 | + self += Self(value) |
| 178 | + raw.removeFirst(numeral.count) |
| 179 | + } |
| 180 | + } |
| 181 | + guard raw.isEmpty else { return nil } |
| 182 | + } |
| 183 | + |
| 184 | + // Substring permutations (romanSS) |
| 185 | + |
| 186 | + init?(romanSShPrF number: String) { |
| 187 | + self = 0 |
| 188 | + var raw = Substring(number) |
| 189 | + for (numeral, value) in romanTable { |
| 190 | + while raw.hasPrefix(numeral) { |
| 191 | + self += Self(value) |
| 192 | + raw.removeFirst(numeral.count) |
| 193 | + } |
| 194 | + } |
| 195 | + guard raw.isEmpty else { return nil } |
| 196 | + } |
| 197 | + |
| 198 | + init?(romanSShPdF number: String) { |
| 199 | + self = 0 |
| 200 | + var raw = Substring(number) |
| 201 | + for (numeral, value) in romanTable { |
| 202 | + while raw.hasPrefix(numeral) { |
| 203 | + self += Self(value) |
| 204 | + raw = raw.dropFirst(numeral.count) |
| 205 | + } |
| 206 | + } |
| 207 | + guard raw.isEmpty else { return nil } |
| 208 | + } |
| 209 | + |
| 210 | + init?(romanSSsWdF number: String) { |
| 211 | + self = 0 |
| 212 | + var raw = Substring(number) |
| 213 | + for (numeral, value) in romanTable { |
| 214 | + while raw.starts(with: numeral) { |
| 215 | + self += Self(value) |
| 216 | + raw = raw.dropFirst(numeral.count) |
| 217 | + } |
| 218 | + } |
| 219 | + guard raw.isEmpty else { return nil } |
| 220 | + } |
| 221 | + |
| 222 | + init?(romanSSsWrF number: String) { |
| 223 | + self = 0 |
| 224 | + var raw = Substring(number) |
| 225 | + for (numeral, value) in romanTable { |
| 226 | + while raw.starts(with: numeral) { |
| 227 | + self += Self(value) |
| 228 | + raw.removeFirst(numeral.count) |
| 229 | + } |
| 230 | + } |
| 231 | + guard raw.isEmpty else { return nil } |
| 232 | + } |
| 233 | + |
| 234 | + // UTF8View SubSequence |
| 235 | + |
| 236 | + init?(romanU8SSsWdF number: String) { |
| 237 | + self = 0 |
| 238 | + var raw = number.utf8[...] |
| 239 | + for (numeral, value) in romanTableUTF8 { |
| 240 | + while raw.starts(with: numeral) { |
| 241 | + self += Self(value) |
| 242 | + raw = raw.dropFirst(numeral.count) |
| 243 | + } |
| 244 | + } |
| 245 | + guard raw.isEmpty else { return nil } |
| 246 | + } |
| 247 | + |
| 248 | + init?(romanU8SSsWrF number: String) { |
| 249 | + self = 0 |
| 250 | + var raw = number.utf8[...] |
| 251 | + for (numeral, value) in romanTableUTF8 { |
| 252 | + while raw.starts(with: numeral) { |
| 253 | + self += Self(value) |
| 254 | + raw.removeFirst(numeral.count) |
77 | 255 | }
|
78 |
| - return nil |
79 | 256 | }
|
| 257 | + guard raw.isEmpty else { return nil } |
80 | 258 | }
|
81 | 259 | }
|
82 | 260 |
|
83 |
| -@inline(never) |
84 |
| -func checkRomanNumerals(upTo limit: Int) { |
85 |
| - for i in 0 ..< limit { |
86 |
| - CheckResults(Int(romanNumeral: identity(i.romanNumeral)) == i) |
| 261 | +extension BinaryInteger { |
| 262 | + // FP-style |
| 263 | + // Following is a translation of the imperative algorithm into functional |
| 264 | + // style: for-in loop is replaced with reduction and while loop with recusion. |
| 265 | + // XXX: These functions are not tail call optimized... 🤷♂️ (ARC on Strings?) |
| 266 | + |
| 267 | + typealias State = (number: String, value: Self) |
| 268 | + typealias Roman = (numeral: String, value: Int) |
| 269 | + |
| 270 | + // Classic functional style with reduce |
| 271 | + |
| 272 | + static func parseRomanNumeral(_ running: State, candidate r: Roman) -> State { |
| 273 | + guard running.number.hasPrefix(r.numeral) else { return running } |
| 274 | + return parseRomanNumeral((String(running.number.dropFirst(r.numeral.count)), |
| 275 | + running.value + Self(r.value)), candidate: r) |
| 276 | + } |
| 277 | + |
| 278 | + static func buildRomanNumeral(_ running: State, candidate r: Roman) -> State { |
| 279 | + guard running.value >= r.value else { return running } |
| 280 | + return buildRomanNumeral( |
| 281 | + (running.number + r.numeral, running.value - Self(r.value)), candidate: r) |
| 282 | + } |
| 283 | + |
| 284 | + var romanReduce: String { |
| 285 | + return romanTable.reduce(("", self), Self.buildRomanNumeral).number |
| 286 | + } |
| 287 | + |
| 288 | + init?(romanReduce number: String) { |
| 289 | + let (remainder, value) = romanTable.reduce( |
| 290 | + (number, Self(0)), Self.parseRomanNumeral) |
| 291 | + guard remainder.isEmpty else { return nil } |
| 292 | + self = value |
| 293 | + } |
| 294 | + |
| 295 | + // Swifty mutable hybrid functional style with reduce(into:) |
| 296 | + |
| 297 | + static func parseRomanNumeral(_ running: inout State, candidate r: Roman) { |
| 298 | + guard running.number.hasPrefix(r.numeral) else { return } |
| 299 | + running.number = String(running.number.dropFirst(r.numeral.count)) |
| 300 | + running.value += Self(r.value) |
| 301 | + parseRomanNumeral(&running, candidate: r) |
| 302 | + } |
| 303 | + |
| 304 | + static func buildRomanNumeral(_ running: inout State, candidate r: Roman) { |
| 305 | + guard running.value >= r.value else { return } |
| 306 | + running.value -= Self(r.value) |
| 307 | + running.number += r.numeral |
| 308 | + buildRomanNumeral(&running, candidate: r) |
| 309 | + } |
| 310 | + |
| 311 | + var romanReduceInto: String { |
| 312 | + return romanTable.reduce(into: ("", self), Self.buildRomanNumeral).number |
| 313 | + } |
| 314 | + |
| 315 | + init?(romanReduceInto number: String) { |
| 316 | + let (remainder, value) = romanTable.reduce(into: |
| 317 | + (number, Self(0)), Self.parseRomanNumeral) |
| 318 | + guard remainder.isEmpty else { return nil } |
| 319 | + self = value |
87 | 320 | }
|
88 | 321 | }
|
89 | 322 |
|
90 |
| -@inline(never) |
91 |
| -public func run_RomanNumbers(_ N: Int) { |
92 |
| - for _ in 0 ..< 10 * N { |
93 |
| - checkRomanNumerals(upTo: 1100) |
| 323 | +// Parsing with Dictionary and map reduce. |
| 324 | +// See `fromRoman2` https://www.rosettacode.org/wiki/Roman_numerals/Decode#Scala |
| 325 | + |
| 326 | +let romanDigits: Dictionary<Character, Int> = [ |
| 327 | + "I": 1, "V": 5, "X": 10, "L": 50, "C": 100, "D": 500, "M": 1000 |
| 328 | +] |
| 329 | + |
| 330 | +extension BinaryInteger { |
| 331 | + typealias RunningSum = (sum: Self, last: Self) |
| 332 | + |
| 333 | + static func sumRomanDigits(r: RunningSum?, digitValue: Self?) -> RunningSum? { |
| 334 | + switch (r, digitValue) { |
| 335 | + case let (r?, value?): |
| 336 | + return (r.sum + value - (r.last < value ? 2 * r.last : 0), value) |
| 337 | + default: |
| 338 | + return nil |
| 339 | + } |
| 340 | + } |
| 341 | + |
| 342 | + init?(romanMapReduce number: String) { |
| 343 | + guard let r = (number |
| 344 | + .lazy // brings about 2x improvement over eager |
| 345 | + .map { romanDigits[$0].map { Self($0) } } |
| 346 | + .reduce((Self(0), Self(0)), Self.sumRomanDigits) |
| 347 | + ) else { return nil } |
| 348 | + self = r.sum |
94 | 349 | }
|
95 | 350 | }
|
0 commit comments