Skip to content

Commit 0edd560

Browse files
committed
[benchmark] RomanNumbers Redux
Rewrite of the `RomanNumbers` benchmark that fixes error in the `position` which was always reset to 0 after `continue outer`, by swapping the `while` and `for-in` loops into more natural order, which eliminates the need for using `position` at all. This is based on examples from [Rosetta Code](https://www.rosettacode.org/wiki/Roman_numerals/Decode#Swift). The coverage has been expanded to measure performance of `String`/`Substring`/`UTF8View` with very short string arguments and methods: `hasPrefix`/`starts(with:)`, `removeFirst`/`dropFirst` and `String.append()`, as well as functional variants of the fastest imperative version. The `Roman.Substring.startsWith.dropFirst` variant tests the same code paths as the original `RomanNumbers` benchmark, but the name is changed because of the bug-fix and lowered workload (to run in less than 1000 μs). For comparison, there's one extra variant with character based parsing algorithm: `Roman.DictCharInt.map.reduce`.
1 parent ff0d764 commit 0edd560

File tree

1 file changed

+306
-51
lines changed

1 file changed

+306
-51
lines changed

benchmark/single-source/RomanNumbers.swift

Lines changed: 306 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -12,84 +12,339 @@
1212

1313
import TestsUtils
1414

15-
//
1615
// 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().
1919
//
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
2025

2126
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:
2282
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),
2697
]
2798

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+
28108
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__,
41112
"I": 1,
42113
]
43114

115+
let romanTableUTF8 = romanTable.map { ($0.utf8, $1) }
116+
44117
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+
45122
var romanNumeral: String {
46123
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)
58129
}
59-
fatalError("Unreachable")
60130
}
61131
return result
62132
}
63133

64-
init?(romanNumeral: String) {
134+
// String permutations (romanS)
135+
136+
init?(romanShPrF number: String) {
65137
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)
77255
}
78-
return nil
79256
}
257+
guard raw.isEmpty else { return nil }
80258
}
81259
}
82260

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
87320
}
88321
}
89322

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
94349
}
95350
}

0 commit comments

Comments
 (0)