diff --git a/benchmark/CMakeLists.txt b/benchmark/CMakeLists.txt index 62d8aa2473c9e..3985dd515669b 100644 --- a/benchmark/CMakeLists.txt +++ b/benchmark/CMakeLists.txt @@ -42,6 +42,7 @@ set(SWIFT_BENCH_MODULES single-source/BinaryFloatingPointConversionFromBinaryInteger single-source/BinaryFloatingPointProperties single-source/BitCount + single-source/Breadcrumbs single-source/ByteSwap single-source/COWTree single-source/COWArrayGuaranteedParameterOverhead diff --git a/benchmark/single-source/Breadcrumbs.swift b/benchmark/single-source/Breadcrumbs.swift new file mode 100644 index 0000000000000..36c9122689e60 --- /dev/null +++ b/benchmark/single-source/Breadcrumbs.swift @@ -0,0 +1,498 @@ +//===--- Breadcrumbs.swift ------------------------------------*- swift -*-===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2018 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import TestsUtils + +// Tests the performance of String's memoized UTF-8 to UTF-16 index conversion +// breadcrumbs. These are used to speed up range- and positional access through +// conventional NSString APIs. + +public let Breadcrumbs: [BenchmarkInfo] = [ + UTF16ToIdx(workload: longASCIIWorkload, count: 5_000).info, + UTF16ToIdx(workload: longMixedWorkload, count: 5_000).info, + IdxToUTF16(workload: longASCIIWorkload, count: 5_000).info, + IdxToUTF16(workload: longMixedWorkload, count: 5_000).info, + + UTF16ToIdxRange(workload: longASCIIWorkload, count: 1_000).info, + UTF16ToIdxRange(workload: longMixedWorkload, count: 1_000).info, + IdxToUTF16Range(workload: longASCIIWorkload, count: 1_000).info, + IdxToUTF16Range(workload: longMixedWorkload, count: 1_000).info, + + CopyUTF16CodeUnits(workload: asciiWorkload, count: 500).info, + CopyUTF16CodeUnits(workload: mixedWorkload, count: 500).info, + + MutatedUTF16ToIdx(workload: asciiWorkload, count: 50).info, + MutatedUTF16ToIdx(workload: mixedWorkload, count: 50).info, + MutatedIdxToUTF16(workload: asciiWorkload, count: 50).info, + MutatedIdxToUTF16(workload: mixedWorkload, count: 50).info, +] + +extension String { + func forceNativeCopy() -> String { + var result = String() + result.reserveCapacity(64) + result.append(self) + return result + } +} + +let seed = 0x12345678 + +/// A linear congruential PRNG. +struct LCRNG: RandomNumberGenerator { + private var state: UInt64 + + init(seed: Int) { + state = UInt64(truncatingIfNeeded: seed) + for _ in 0..<10 { _ = next() } + } + + mutating func next() -> UInt64 { + state = 2862933555777941757 &* state &+ 3037000493 + return state + } +} + +extension Collection { + /// Returns a randomly ordered array of random non-overlapping index ranges + /// that cover this collection entirely. + /// + /// Note: some of the returned ranges may be empty. + func randomIndexRanges( + count: Int, + using generator: inout R + ) -> [Range] { + // Load indices into a buffer to prevent quadratic performance with + // forward-only collections. FIXME: Eliminate this if Self conforms to RAC. + let indices = Array(self.indices) + var cuts: [Index] = (0 ..< count - 1).map { _ in + indices.randomElement(using: &generator)! + } + cuts.append(self.startIndex) + cuts.append(self.endIndex) + cuts.sort() + let ranges = (0 ..< count).map { cuts[$0] ..< cuts[$0 + 1] } + return ranges.shuffled(using: &generator) + } +} + +struct Workload { + let name: String + let string: String +} + +class BenchmarkBase { + let name: String + let workload: Workload + + var inputString: String = "" + + init(name: String, workload: Workload) { + self.name = name + self.workload = workload + } + + var label: String { + return "\(name).\(workload.name)" + } + + func setUp() { + self.inputString = workload.string.forceNativeCopy() + } + + func tearDown() { + self.inputString = "" + } + + func run(iterations: Int) { + fatalError("unimplemented abstract method") + } + + var info: BenchmarkInfo { + return BenchmarkInfo( + name: self.label, + runFunction: self.run(iterations:), + tags: [.validation, .api, .String], + setUpFunction: self.setUp, + tearDownFunction: self.tearDown) + } +} + +//============================================================================== +// Workloads +//============================================================================== + +let asciiBase = #""" + * Debugger support. Swift has a `-g` command line switch that turns on + debug info for the compiled output. Using the standard lldb debugger + this will allow single-stepping through Swift programs, printing + backtraces, and navigating through stack frames; all in sync with + the corresponding Swift source code. An unmodified lldb cannot + inspect any variables. + + Example session: + + ``` + $ echo 'println("Hello World")' >hello.swift + $ swift hello.swift -c -g -o hello.o + $ ld hello.o "-dynamic" "-arch" "x86_64" "-macosx_version_min" "10.9.0" \ + -framework Foundation lib/swift/libswift_stdlib_core.dylib \ + lib/swift/libswift_stdlib_posix.dylib -lSystem -o hello + $ lldb hello + Current executable set to 'hello' (x86_64). + (lldb) b top_level_code + Breakpoint 1: where = hello`top_level_code + 26 at hello.swift:1, addre... + (lldb) r + Process 38592 launched: 'hello' (x86_64) + Process 38592 stopped + * thread #1: tid = 0x1599fb, 0x0000000100000f2a hello`top_level_code + ... + frame #0: 0x0000000100000f2a hello`top_level_code + 26 at hello.shi... + -> 1 println("Hello World") + (lldb) bt + * thread #1: tid = 0x1599fb, 0x0000000100000f2a hello`top_level_code + ... + frame #0: 0x0000000100000f2a hello`top_level_code + 26 at hello.shi... + frame #1: 0x0000000100000f5c hello`main + 28 + frame #2: 0x00007fff918605fd libdyld.dylib`start + 1 + frame #3: 0x00007fff918605fd libdyld.dylib`start + 1 + ``` + + Also try `s`, `n`, `up`, `down`. + + * `nil` can now be used without explicit casting. Previously, `nil` had + type `NSObject`, so one would have to write (e.g.) `nil as! NSArray` + to create a `nil` `NSArray`. Now, `nil` picks up the type of its + context. + + * `POSIX.EnvironmentVariables` and `swift.CommandLineArguments` global variables + were merged into a `swift.Process` variable. Now you can access command line + arguments with `Process.arguments`. In order to access environment variables + add `import POSIX` and use `Process.environmentVariables`. + + func _toUTF16Offsets(_ indices: Range) -> Range { + let lowerbound = _toUTF16Offset(indices.lowerBound) + let length = self.utf16.distance( + from: indices.lowerBound, to: indices.upperBound) + return Range( + uncheckedBounds: (lower: lowerbound, upper: lowerbound + length)) + } + 0 swift 0x00000001036b5f58 llvm::sys::PrintStackTrace(llvm::raw_ostream&) + 40 + 1 swift 0x00000001036b50f8 llvm::sys::RunSignalHandlers() + 248 + 2 swift 0x00000001036b6572 SignalHandler(int) + 258 + 3 libsystem_platform.dylib 0x00007fff64010b5d _sigtramp + 29 + 4 libsystem_platform.dylib 0x0000000100000000 _sigtramp + 2617177280 + 5 libswiftCore.dylib 0x0000000107b5d135 $sSh8_VariantV7element2atxSh5IndexVyx_G_tF + 613 + 6 libswiftCore.dylib 0x0000000107c51449 $sShyxSh5IndexVyx_Gcig + 9 + 7 libswiftCore.dylib 0x00000001059d60be $sShyxSh5IndexVyx_Gcig + 4258811006 + 8 swift 0x000000010078801d llvm::MCJIT::runFunction(llvm::Function*, llvm::ArrayRef) + 381 + 9 swift 0x000000010078b0a4 llvm::ExecutionEngine::runFunctionAsMain(llvm::Function*, std::__1::vector, std::__1::allocator >, std::__1::allocator, std::__1::allocator > > > const&, char const* const*) + 1268 + 10 swift 0x00000001000e048c REPLEnvironment::executeSwiftSource(llvm::StringRef, std::__1::vector, std::__1::allocator >, std::__1::allocator, std::__1::allocator > > > const&) + 1532 + 11 swift 0x00000001000dbbd3 swift::runREPL(swift::CompilerInstance&, std::__1::vector, std::__1::allocator >, std::__1::allocator, std::__1::allocator > > > const&, bool) + 1443 + 12 swift 0x00000001000b5341 performCompile(swift::CompilerInstance&, swift::CompilerInvocation&, llvm::ArrayRef, int&, swift::FrontendObserver*, swift::UnifiedStatsReporter*) + 2865 + 13 swift 0x00000001000b38f4 swift::performFrontend(llvm::ArrayRef, char const*, void*, swift::FrontendObserver*) + 3028 + 14 swift 0x000000010006ca44 main + 660 + 15 libdyld.dylib 0x00007fff63e293f1 start + 1 + 16 libdyld.dylib 0x0000000000000008 start + 2619173912 + Illegal instruction: 4 + + """# +let asciiWorkload = Workload( + name: "ASCII", + string: asciiBase) +let longASCIIWorkload = Workload( + name: "longASCII", + string: String(repeating: asciiBase, count: 100)) + +let mixedBase = """ + siebenhundertsiebenundsiebzigtausendsiebenhundertsiebenundsiebzig + ๐Ÿ‘๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ‡จ๐Ÿ‡ฆ๐Ÿ‡ฒ๐Ÿ‡ฝ๐Ÿ‘๐Ÿป๐Ÿ‘๐Ÿผ๐Ÿ‘๐Ÿฝ๐Ÿ‘๐Ÿพ๐Ÿ‘๐Ÿฟ + siebenhundertsiebenundsiebzigtausendsiebenhundertsiebenundsiebzig + ๐Ÿ‘๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ‡จ๐Ÿ‡ฆ๐Ÿ‡ฒ๐Ÿ‡ฝ๐Ÿ‘๐Ÿป๐Ÿ‘๐Ÿผ๐Ÿ‘๐Ÿฝ๐Ÿ‘๐Ÿพ๐Ÿ‘๐Ÿฟthe quick brown fox๐Ÿ‘๐Ÿฟ๐Ÿ‘๐Ÿพ๐Ÿ‘๐Ÿฝ๐Ÿ‘๐Ÿผ๐Ÿ‘๐Ÿป๐Ÿ‡ฒ๐Ÿ‡ฝ๐Ÿ‡จ๐Ÿ‡ฆ๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง๐Ÿ‘ + siebenhundertsiebenundsiebzigtausendsiebenhundertsiebenundsiebzig + ไปŠๅ›žใฎใ‚ขใƒƒใƒ—ใƒ‡ใƒผใƒˆใงSwiftใซๅคงๅน…ใชๆ”น่‰ฏใŒๆ–ฝใ•ใ‚Œใ€ๅฎ‰ๅฎšใ—ใฆใ„ใฆใ—ใ‹ใ‚‚็›ดๆ„Ÿ็š„ใซไฝฟใ†ใ“ใจใŒใงใใ‚‹Appleใƒ—ใƒฉใƒƒใƒˆใƒ•ใ‚ฉใƒผใƒ ๅ‘ใ‘ใƒ—ใƒญใ‚ฐใƒฉใƒŸใƒณใ‚ฐ่จ€่ชžใซใชใ‚Šใพใ—ใŸใ€‚ + Worst thing about working on String is that it breaks *everything*. Asserts, debuggers, and *especially* printf-style debugging ๐Ÿ˜ญ + Swift ๆ˜ฏ้ขๅ‘ Apple ๅนณๅฐ็š„็ผ–็จ‹่ฏญ่จ€๏ผŒๅŠŸ่ƒฝๅผบๅคงไธ”็›ด่ง‚ๆ˜“็”จ๏ผŒ่€Œๆœฌๆฌกๆ›ดๆ–ฐๅฏนๅ…ถ่ฟ›่กŒไบ†ๅ…จ้ขไผ˜ๅŒ–ใ€‚ + siebenhundertsiebenundsiebzigtausendsiebenhundertsiebenundsiebzig + ์ด๋ฒˆ ์—…๋ฐ์ดํŠธ์—์„œ๋Š” ๊ฐ•๋ ฅํ•˜๋ฉด์„œ๋„ ์ง๊ด€์ ์ธ Apple ํ”Œ๋žซํผ์šฉ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์–ธ์–ด์ธ Swift๋ฅผ ์™„๋ฒฝํžˆ ๊ฐœ์„ ํ•˜์˜€์Šต๋‹ˆ๋‹ค. + Worst thing about working on String is that it breaks *everything*. Asserts, debuggers, and *especially* printf-style debugging ๐Ÿ˜ญ + ะฒ ั‡ะฐั‰ะฐั… ัŽะณะฐ ะถะธะป-ะฑั‹ะป ั†ะธั‚ั€ัƒั? ะดะฐ, ะฝะพ ั„ะฐะปัŒัˆะธะฒั‹ะน ัะบะทะตะผะฟะปัั€ + siebenhundertsiebenundsiebzigtausendsiebenhundertsiebenundsiebzig + \u{201c}Hello\u{2010}world\u{2026}\u{201d} + \u{300c}\u{300e}ไปŠๆ—ฅใฏ\u{3001}ไธ–็•Œ\u{3002}\u{300f}\u{300d} + Worst thing about working on String is that it breaks *everything*. Asserts, debuggers, and *especially* printf-style debugging ๐Ÿ˜ญ + + """ +let mixedWorkload = Workload( + name: "Mixed", + string: mixedBase) +let longMixedWorkload = Workload( + name: "longMixed", + string: String(repeating: mixedBase, count: 100)) + + +//============================================================================== +// Benchmarks +//============================================================================== + +/// Convert `count` random UTF-16 offsets into String indices. +class UTF16ToIdx: BenchmarkBase { + let count: Int + var inputOffsets: [Int] = [] + + init(workload: Workload, count: Int) { + self.count = count + super.init(name: "Breadcrumbs.UTF16ToIdx", workload: workload) + } + + override func setUp() { + super.setUp() + var rng = LCRNG(seed: seed) + let range = 0 ..< inputString.utf16.count + inputOffsets = Array(range.shuffled(using: &rng).prefix(count)) + } + + override func tearDown() { + super.tearDown() + inputOffsets = [] + } + + @inline(never) + override func run(iterations: Int) { + for _ in 0 ..< iterations { + for offset in inputOffsets { + blackHole(inputString._toUTF16Index(offset)) + } + } + } +} + +/// Convert `count` random String indices into UTF-16 offsets. +class IdxToUTF16: BenchmarkBase { + let count: Int + var inputIndices: [String.Index] = [] + + init(workload: Workload, count: Int) { + self.count = count + super.init(name: "Breadcrumbs.IdxToUTF16", workload: workload) + } + + override func setUp() { + super.setUp() + var rng = LCRNG(seed: seed) + inputIndices = Array(inputString.indices.shuffled(using: &rng).prefix(count)) + } + + override func tearDown() { + super.tearDown() + inputIndices = [] + } + + @inline(never) + override func run(iterations: Int) { + for _ in 0 ..< iterations { + for index in inputIndices { + blackHole(inputString._toUTF16Offset(index)) + } + } + } +} + +/// Split a string into `count` random slices and convert their UTF-16 offsets +/// into String index ranges. +class UTF16ToIdxRange: BenchmarkBase { + let count: Int + var inputOffsets: [Range] = [] + + init(workload: Workload, count: Int) { + self.count = count + super.init(name: "Breadcrumbs.UTF16ToIdxRange", workload: workload) + } + + override func setUp() { + super.setUp() + var rng = LCRNG(seed: seed) + inputOffsets = ( + 0 ..< inputString.utf16.count + ).randomIndexRanges(count: count, using: &rng) + } + + override func tearDown() { + super.tearDown() + inputOffsets = [] + } + + @inline(never) + override func run(iterations: Int) { + for _ in 0 ..< iterations { + for range in inputOffsets { + blackHole(inputString._toUTF16Indices(range)) + } + } + } +} + +/// Split a string into `count` random slices and convert their index ranges +/// into into UTF-16 offset pairs. +class IdxToUTF16Range: BenchmarkBase { + let count: Int + var inputIndices: [Range] = [] + + init(workload: Workload, count: Int) { + self.count = count + super.init(name: "Breadcrumbs.IdxToUTF16Range", workload: workload) + } + + override func setUp() { + super.setUp() + var rng = LCRNG(seed: seed) + inputIndices = self.inputString.randomIndexRanges(count: count, using: &rng) + } + + override func tearDown() { + super.tearDown() + inputIndices = [] + } + + @inline(never) + override func run(iterations: Int) { + for _ in 0 ..< iterations { + for range in inputIndices { + blackHole(inputString._toUTF16Offsets(range)) + } + } + } +} + + +class CopyUTF16CodeUnits: BenchmarkBase { + let count: Int + var inputIndices: [Range] = [] + var outputBuffer: [UInt16] = [] + + init(workload: Workload, count: Int) { + self.count = count + super.init(name: "Breadcrumbs.CopyUTF16CodeUnits", workload: workload) + } + + override func setUp() { + super.setUp() + var rng = LCRNG(seed: seed) + inputIndices = ( + 0 ..< inputString.utf16.count + ).randomIndexRanges(count: count, using: &rng) + outputBuffer = Array(repeating: 0, count: inputString.utf16.count) + } + + override func tearDown() { + super.tearDown() + inputIndices = [] + } + + @inline(never) + override func run(iterations: Int) { + outputBuffer.withUnsafeMutableBufferPointer { buffer in + for _ in 0 ..< iterations { + for range in inputIndices { + inputString._copyUTF16CodeUnits( + into: UnsafeMutableBufferPointer(rebasing: buffer[range]), + range: range) + } + } + } + } +} + +/// This is like `UTF16ToIdx` but appends to the string after every index +/// conversion. In effect, this tests breadcrumb creation performance. +class MutatedUTF16ToIdx: BenchmarkBase { + let count: Int + var inputOffsets: [Int] = [] + + init(workload: Workload, count: Int) { + self.count = count + super.init( + name: "Breadcrumbs.MutatedUTF16ToIdx", + workload: workload) + } + + override func setUp() { + super.setUp() + var generator = LCRNG(seed: seed) + let range = 0 ..< inputString.utf16.count + inputOffsets = Array(range.shuffled(using: &generator).prefix(count)) + } + + override func tearDown() { + super.tearDown() + inputOffsets = [] + } + + @inline(never) + override func run(iterations: Int) { + var flag = true + for _ in 0 ..< iterations { + var string = inputString + for offset in inputOffsets { + blackHole(string._toUTF16Index(offset)) + if flag { + string.append(" ") + } else { + string.removeLast() + } + flag.toggle() + } + } + } +} + + +/// This is like `IdxToUTF16` but appends to the string after every index +/// conversion. In effect, this tests breadcrumb creation performance. +class MutatedIdxToUTF16: BenchmarkBase { + let count: Int + var inputIndices: [String.Index] = [] + + init(workload: Workload, count: Int) { + self.count = count + super.init( + name: "Breadcrumbs.MutatedIdxToUTF16", + workload: workload) + } + + override func setUp() { + super.setUp() + var rng = LCRNG(seed: seed) + inputIndices = Array(inputString.indices.shuffled(using: &rng).prefix(count)) + } + + override func tearDown() { + super.tearDown() + inputIndices = [] + } + + @inline(never) + override func run(iterations: Int) { + var flag = true + for _ in 0 ..< iterations { + var string = inputString + for index in inputIndices { + blackHole(string._toUTF16Offset(index)) + if flag { + string.append(" ") + flag = false + } else { + string.removeLast() + flag = true + } + } + } + } +} diff --git a/benchmark/utils/main.swift b/benchmark/utils/main.swift index aec148045a6cf..aca1c1499fd1b 100644 --- a/benchmark/utils/main.swift +++ b/benchmark/utils/main.swift @@ -30,6 +30,7 @@ import ArraySubscript import BinaryFloatingPointConversionFromBinaryInteger import BinaryFloatingPointProperties import BitCount +import Breadcrumbs import ByteSwap import COWTree import COWArrayGuaranteedParameterOverhead @@ -195,6 +196,7 @@ registerBenchmark(BinaryFloatingPointPropertiesBinade) registerBenchmark(BinaryFloatingPointPropertiesNextUp) registerBenchmark(BinaryFloatingPointPropertiesUlp) registerBenchmark(BitCount) +registerBenchmark(Breadcrumbs) registerBenchmark(ByteSwap) registerBenchmark(COWTree) registerBenchmark(COWArrayGuaranteedParameterOverhead)