diff --git a/Sources/PenguinParallel/NonblockingThreadPool/NonBlockingThreadPool.swift b/Sources/PenguinParallel/NonblockingThreadPool/NonBlockingThreadPool.swift index 446a319a..0046ef88 100644 --- a/Sources/PenguinParallel/NonblockingThreadPool/NonBlockingThreadPool.swift +++ b/Sources/PenguinParallel/NonblockingThreadPool/NonBlockingThreadPool.swift @@ -64,7 +64,13 @@ public class NonBlockingThreadPool: ComputeThr let totalThreadCount: Int let externalFastPathThreadCount: Int var externalFastPathThreadSeenCount: Int = 0 - let coprimes: [Int] + + /// A vocabulary of step sizes used to randomly search the list of queues for work to steal. + /// + /// When looking for work, pool threads will pick a random step size and traverse the `queues` + /// looking to steal work. The coprime property ensures that every queue will be examined, but the + /// stealing threads will traverse in diverging orders, avoiding thundering herds. + let workStealingStrides: [Int] let queues: [Queue] var cancelledStorage: AtomicUInt64 var blockedCountStorage: AtomicUInt64 @@ -99,7 +105,7 @@ public class NonBlockingThreadPool: ComputeThr let totalThreadCount = threadCount + externalFastPathThreadCount self.totalThreadCount = totalThreadCount self.externalFastPathThreadCount = externalFastPathThreadCount - self.coprimes = positiveCoprimes(totalThreadCount) + self.stepSizes = totalThreadCount.lesserPositiveCoprimes self.queues = (0.. { self.pool = pool self.totalThreadCount = pool.totalThreadCount self.workerThreadCount = pool.totalThreadCount - pool.externalFastPathThreadCount - self.coprimes = pool.coprimes + self.stepSizes = pool.stepSizes self.queues = pool.queues self.condition = pool.condition self.rng = PCGRandomNumberGenerator(state: UInt64(threadId)) @@ -469,7 +475,7 @@ fileprivate final class PerThreadState { let totalThreadCount: Int let workerThreadCount: Int - let coprimes: [Int] + let stepSizes: [Int] let queues: [NonBlockingThreadPool.Queue] let condition: NonblockingCondition @@ -480,7 +486,7 @@ fileprivate final class PerThreadState { func steal() -> Task? { let r = rng.next() var selectedThreadId = Int(r.reduced(into: UInt64(totalThreadCount))) - let step = coprimes[Int(r.reduced(into: UInt64(coprimes.count)))] + let step = stepSizes[Int(r.reduced(into: UInt64(stepSizes.count)))] assert( step < totalThreadCount, "step: \(step), pool threadcount: \(totalThreadCount)") @@ -545,7 +551,7 @@ fileprivate final class PerThreadState { private func findNonEmptyQueueIndex() -> Int? { let r = rng.next() let increment = - totalThreadCount == 1 ? 1 : coprimes[Int(r.reduced(into: UInt64(coprimes.count)))] + totalThreadCount == 1 ? 1 : stepSizes[Int(r.reduced(into: UInt64(stepSizes.count)))] var threadIndex = Int(r.reduced(into: UInt64(totalThreadCount))) for _ in 0.. [Int] { - var coprimes = [Int]() - for i in 1...n { - var a = i - var b = n - // If GCD(a, b) == 1, then a and b are coprimes. - while b != 0 { - let tmp = a - a = b - b = tmp % b - } - if a == 1 { coprimes.append(i) } - } - return coprimes -} diff --git a/Sources/PenguinStructures/NumberOperations.swift b/Sources/PenguinStructures/NumberOperations.swift new file mode 100644 index 00000000..34dd10e9 --- /dev/null +++ b/Sources/PenguinStructures/NumberOperations.swift @@ -0,0 +1,112 @@ +// Copyright 2020 Penguin Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +extension BinaryInteger { + /// A collection of the positive integers that are coprime with `self`. + /// + /// Definition: Two numbers are coprime if their GCD is 1. + public var positiveCoprimes: PositiveCoprimes { .init(self) } + + /// Returns positive integers that are coprime with, and less than, `self`. + /// + /// The returned values are all unique modulo `self`. + /// + /// - SeeAlso: `positiveCoprimes`. + public var lesserPositiveCoprimes: [Self] { + return positiveCoprimes.prefix { $0 < self } + } +} + +/// The positive values that are coprime with *N*. +/// +/// Example: +/// ``` +/// print(Array(10.positiveCoprimes.prefix(8))) // [1, 3, 7, 9, 11, 13, 17, 19] +/// ``` +/// +/// Note: Although there are infinitely many positive prime numbers, `PositiveCoprimes` is bounded +/// by the maximum representable integer in `Domain` (if such a limit exists, such as the +/// `FixedWidthInteger` types). +public struct PositiveCoprimes: Collection { + // TODO: Specialize SubSequence for efficiency. + + /// The number to find coprimes relative to. + public let target: Domain + + /// The index into the collection of positive coprimes are the coprimes themselves. + /// + /// Note: the indices are not dense or contiguous in `Domain`. + public typealias Index = Domain + + /// Creates a collection of numbers coprime relative to `target`. + internal init(_ target: Domain) { + self.target = target < 0 ? -target : target + } + + /// `Int.max`, as there are infinitely many prime numbers, and thus infinitely many coprimes + /// to a given target. + public var count: Int { + if _slowPath(target == 0) { return 0 } + return Int.max + } + + /// Accesses the coprime at `index`. + public subscript(index: Index) -> Domain { index } + + /// The index of the first element. + public var startIndex: Index { 1 } + + /// The largest possible element of `Domain` up to `UInt64.max`. + /// + /// Note: if a `BinaryInteger` larger than `UInt64.max` is used, the `endIndex` might leave off + /// potentially useful integers. + public var endIndex: Index { + if _slowPath(target == 0) { return startIndex } + return Domain(clamping: UInt64.max) + } + + /// Returns the position after `i`. + public func index(after i: Index) -> Index { + var nextCandidate = index + while true { + nextCandidate += 1 + if gcd(nextCandidate, target) == 1 { return nextCandidate } + } + } +} + +/// Returns the greatest common divisor of `a` and `b`. +/// +/// - Complexity: O(n^2) where `n` is the number of bits in `Domain`. +// TODO: Switch to the Binary GCD algorithm which avoids expensive modulo operations. +public func gcd(_ a: Domain, _ b: Domain) -> Domain { + var a = a + var b = b + + if a < 0 { + a = -a + } + + if b < 0 { + b = -b + } + + if a > b { + swap(&a, &b) + } + while b != 0 { + (a, b) = (b, a % b) + } + return a +} diff --git a/Tests/PenguinStructuresTests/NumberOperationsTests.swift b/Tests/PenguinStructuresTests/NumberOperationsTests.swift new file mode 100644 index 00000000..f59c34a4 --- /dev/null +++ b/Tests/PenguinStructuresTests/NumberOperationsTests.swift @@ -0,0 +1,48 @@ +// Copyright 2020 Penguin Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import PenguinStructures +import XCTest + +final class NumberOperationsTests: XCTestCase { + func testCoprimes() { + XCTAssertEqual([1, 3, 5, 7], 8.lesserPositiveCoprimes) + XCTAssertEqual([1, 2, 3, 4], 5.lesserPositiveCoprimes) + XCTAssertEqual([1, 2, 4, 7, 8, 11, 13, 14], 15.lesserPositiveCoprimes) + XCTAssertEqual([], 1.lesserPositiveCoprimes) + + XCTAssertEqual([], (-8).lesserPositiveCoprimes) + + XCTAssertEqual([1, 3, 5, 7, 9, 11, 13, 15], Array(8.positiveCoprimes.prefix(8))) + XCTAssertEqual([1, 3, 5, 7, 9, 11, 13, 15], Array((-8).positiveCoprimes.prefix(8))) + XCTAssertEqual(Array(1...8), Array(1.positiveCoprimes.prefix(8))) + + XCTAssertEqual([], Array(0.positiveCoprimes.prefix(8))) + } + + func testGCD() { + XCTAssertEqual(3, gcd(0, 3)) + XCTAssertEqual(3, gcd(3, 0)) + XCTAssertEqual(4, gcd(-4, 8)) + + XCTAssertEqual(3, gcd(UInt(15), 3)) + + XCTAssertEqual(5, gcd(-5, 0)) + } + + static var allTests = [ + ("testCoprimes", testCoprimes), + ("testGCD", testGCD), + ] +} diff --git a/Tests/PenguinStructuresTests/XCTestManifests.swift b/Tests/PenguinStructuresTests/XCTestManifests.swift index 8d1aae2d..b4dab739 100644 --- a/Tests/PenguinStructuresTests/XCTestManifests.swift +++ b/Tests/PenguinStructuresTests/XCTestManifests.swift @@ -31,6 +31,7 @@ import XCTest testCase(NominalElementDictionaryTests.allTests), testCase(HeapTests.allTests), testCase(HierarchicalCollectionTests.allTests), + testCase(NumberOperationsTests.allTests), testCase(PCGRandomNumberGeneratorTests.allTests), testCase(RandomTests.allTests), testCase(TupleTests.allTests),