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

Refactor co-primes algorithm to be generic & avoid allocation. #36

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,13 @@ public class NonBlockingThreadPool<Environment: ConcurrencyPlatform>: 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
Expand Down Expand Up @@ -99,7 +105,7 @@ public class NonBlockingThreadPool<Environment: ConcurrencyPlatform>: ComputeThr
let totalThreadCount = threadCount + externalFastPathThreadCount
self.totalThreadCount = totalThreadCount
self.externalFastPathThreadCount = externalFastPathThreadCount
self.coprimes = positiveCoprimes(totalThreadCount)
self.stepSizes = totalThreadCount.lesserPositiveCoprimes
self.queues = (0..<totalThreadCount).map { _ in Queue.make() }
self.cancelledStorage = AtomicUInt64()
self.blockedCountStorage = AtomicUInt64()
Expand Down Expand Up @@ -453,7 +459,7 @@ fileprivate final class PerThreadState<Environment: ConcurrencyPlatform> {
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))
Expand All @@ -469,7 +475,7 @@ fileprivate final class PerThreadState<Environment: ConcurrencyPlatform> {

let totalThreadCount: Int
let workerThreadCount: Int
let coprimes: [Int]
let stepSizes: [Int]
let queues: [NonBlockingThreadPool<Environment>.Queue]
let condition: NonblockingCondition<Environment>

Expand All @@ -480,7 +486,7 @@ fileprivate final class PerThreadState<Environment: ConcurrencyPlatform> {
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)")

Expand Down Expand Up @@ -545,7 +551,7 @@ fileprivate final class PerThreadState<Environment: ConcurrencyPlatform> {
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..<totalThreadCount {
if !queues[threadIndex].isEmpty { return threadIndex }
Expand Down

This file was deleted.

112 changes: 112 additions & 0 deletions Sources/PenguinStructures/NumberOperations.swift
Original file line number Diff line number Diff line change
@@ -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<Self> { .init(self) }

/// Returns positive integers that are coprime with, and less than, `self`.
///
saeta marked this conversation as resolved.
Show resolved Hide resolved
/// 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<Domain: BinaryInteger>: 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 {
Copy link
Owner Author

@saeta saeta Jul 10, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: let the default implementation handle this, or do some fancy tricks to actually compute what the numbers of values are, given Domain. (Note: this is tricky, due to not being restricted to BinaryInteger and not FixedWidthInteger.)

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.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just constrain to FixedWidthInteger and access Self.max? Then you can leave out all these messy notes.

public var endIndex: Index {
if _slowPath(target == 0) { return startIndex }
return Domain(clamping: UInt64.max)
saeta marked this conversation as resolved.
Show resolved Hide resolved
}

/// Returns the position after `i`.
public func index(after i: Index) -> Index {
var nextCandidate = index
while true {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can do this without a raw loop; use a range and index(where:)

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<Domain: BinaryInteger>(_ 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
}
48 changes: 48 additions & 0 deletions Tests/PenguinStructuresTests/NumberOperationsTests.swift
Original file line number Diff line number Diff line change
@@ -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 {
saeta marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: Add collection tests.

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),
]
}
1 change: 1 addition & 0 deletions Tests/PenguinStructuresTests/XCTestManifests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down