From fb5c072895401a9c9fc0f93ebface192b781bb08 Mon Sep 17 00:00:00 2001 From: Elias Kauppi Date: Mon, 17 Oct 2022 19:26:35 +0300 Subject: [PATCH 1/2] Add parallel All and Any. --- README.md | 14 +++++++-- common.go | 10 ++++++- common_test.go | 2 +- sliceutils.go | 72 +++++++++++++++++++++++++++++++++++++++++++--- sliceutils_test.go | 56 ++++++++++++++++++++++++++++++++++++ 5 files changed, 145 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 8f1b26a..409d30a 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ This library implements several high-level functions useful for interacting with slices. It reduces boilerplate required by for-loops and variable initializations. -Library takes advantage of Go generics increasing usability by compile-time type-safety. Go version of at least **1.18** is therefore required. +Go version of at least **1.19** is required. ## Examples @@ -83,11 +83,11 @@ dedup := Deduplicate(slice) ### >> _All_ -Returns `true` if all slice elements are evaluated `true` with given argument function. +Returns `true` if all slice elements are evaluated to `true` with given argument function. ### >> _Any_ -Returns `true` if any slice element is evaluated `true` with given argument function. +Returns `true` if any slice element evaluates to `true` with given argument function. ### >> _AreDisjoint_ @@ -213,6 +213,14 @@ Calculates a union set from two slice sets. ## List of parallel functions +### >> _ParAll_ + +Returns `true` if all slice elements are evaluated to `true` with given argument function. Evaluation may be performed in parallel if system has multiple logical CPUs. + +### >> _ParAny_ + +Returns `true` if any slice element evaluates to `true` with given argument function. Evaluation may be performed in parallel if system has multiple logical CPUs. + ### >> _ParMap_ Maps each element through argument function which can modify their type and/or value. Evenly distributes the mapping operation to multiple goroutines. The number of used goroutines is equal to the available number of logical processors. diff --git a/common.go b/common.go index d076ee9..02e5959 100644 --- a/common.go +++ b/common.go @@ -39,7 +39,7 @@ func newSliceDivGen(length, divs int) sliceDivGen { // sub-slice index. // // `divIdx` is expected to be less than the number of divisions. -func (sdg sliceDivGen) get(divIdx int) (int, int) { +func (sdg sliceDivGen) offsetAndLength(divIdx int) (int, int) { if divIdx < sdg.firstPartDivs { offset := (sdg.minDivLen + 1) * divIdx return offset, sdg.minDivLen + 1 @@ -48,3 +48,11 @@ func (sdg sliceDivGen) get(divIdx int) (int, int) { return offset, sdg.minDivLen } } + +// Gets start and end indexes in the original slice for given sub-slice index. +// +// `divIdx` is expected to be less than the number of divisions. +func (sdg sliceDivGen) startAndEnd(divIdx int) (int, int) { + offset, length := sdg.offsetAndLength(divIdx) + return offset, offset + length +} diff --git a/common_test.go b/common_test.go index b049aad..edd2438 100644 --- a/common_test.go +++ b/common_test.go @@ -206,7 +206,7 @@ func TestSliceDivGen(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { for i, expected := range testCase.expects { - offset, length := testCase.gen.get(i) + offset, length := testCase.gen.offsetAndLength(i) assert.Equal(t, expected.offset, offset) assert.Equal(t, expected.length, length) } diff --git a/sliceutils.go b/sliceutils.go index 14e2948..e9cafe7 100644 --- a/sliceutils.go +++ b/sliceutils.go @@ -3,6 +3,7 @@ package sliceutils import ( "runtime" "sync" + "sync/atomic" ) // Returns true if all slice elements are evaluated true with given evaluator @@ -488,6 +489,70 @@ func Union[T comparable](lhs, rhs []T) []T { // PARALLEL FUNCTIONS // //////////////////////// +// Returns true if all slice elements are evaluated to true with given +// evaluator function. Slice processing is divided evenly to number of +// goroutines. +// +// Returns as soon as element evaluating to `false` is found e.g. function is +// short-circuiting. +// +// Order of evaluations is undefined and shall not be depended on. Returns true +// on nil slice. Panics on nil evaluator function. +func ParAll[T any](slice []T, allFn func(T) bool) bool { + // Divide slice by the number of logical CPUs. + divs := runtime.NumCPU() + sliceDivGen := newSliceDivGen(len(slice), divs) + + // Create a waitgroup for waiting goroutines to finish. + var wg sync.WaitGroup + wg.Add(divs) + + // Initialize result flag. + var allFlag atomic.Bool + allFlag.Store(true) + + // Loop over all divisions. + for divIdx := 0; divIdx < divs; divIdx++ { + // Start goroutine for processing a sub-slice. + go func(divIdx int) { + // Notify goroutine has finished in the end. + defer wg.Done() + + // Loop over sub-slice and check if `false` element is found. + // Checking flag after every iteration allows early termination with O(n) + // execution time instead of Θ(n). + // Question still exists whether the flag is worth checking after every + // iteration or some variable N amount of iterations. + start, end := sliceDivGen.startAndEnd(divIdx) + for i := start; i < end && allFlag.Load(); i++ { + if !allFn(slice[i]) { + // `false` element was found. Stop processing the slice. + allFlag.Store(false) + return + } + } + }(divIdx) + } + // Wait until all goroutines have finished. + wg.Wait() + + // Return result. + return allFlag.Load() +} + +// Returns true if any slice element is evaluated to true with given +// evaluator function. Slice processing may be divided to number of goroutines. +// +// Returns as soon as element evaluating to `true` is found e.g. function is +// short-circuiting. +// +// Order of evaluations is undefined and shall not be depended on. Returns false +// on nil slice. Panics on nil evaluator function. +func ParAny[T any](slice []T, anyFn func(T) bool) bool { + // Call ParAll just inverting all results. + return !ParAll(slice, func(val T) bool { return !anyFn(val) }) +} + // Maps each slice value with a mapping function and divides the slice by the // number of logical processors to evenly distribute work. // @@ -510,16 +575,15 @@ func ParMap[T, U any](slice []T, mapFn func(T) U) []U { var wg sync.WaitGroup wg.Add(divs) - // Loop all divisions + // Loop over all divisions. for divIdx := 0; divIdx < divs; divIdx++ { // Start goroutine for mapping a sub-slice. go func(divIdx int) { // Notify goroutine has finished mapping in the end. defer wg.Done() - // Get division specific offset and length for the sub-slice. - offset, length := sliceDivGen.get(divIdx) - start, end := offset, offset+length + // Get division specific start and end indexes for the sub-slice. + start, end := sliceDivGen.startAndEnd(divIdx) // Map. mappedSubSlice := Map(slice[start:end], mapFn) diff --git a/sliceutils_test.go b/sliceutils_test.go index 4999124..b4d2858 100644 --- a/sliceutils_test.go +++ b/sliceutils_test.go @@ -722,6 +722,62 @@ func TestUnion(t *testing.T) { // PARALLEL FUNCTIONS // //////////////////////// +func TestParAll(t *testing.T) { + t.Run("All elements evaluate to true", func(t *testing.T) { + slice := []int{1, 4, 6, 2, 3, 7} + allPositive := ParAll(slice, func(i int) bool { return i > 0 }) + assert.True(t, allPositive) + }) + + t.Run("Some elements don't evaluate to true", func(t *testing.T) { + slice := []int{1, 4, 6, -2, 3, 7} + allPositive := ParAll(slice, func(i int) bool { return i > 0 }) + assert.False(t, allPositive) + }) + + t.Run("An element evaluates to false on large array", func(t *testing.T) { + slice := Generate(1000, func(idx int) int { return idx + 1 }) + slice[500] = 0 + + allPositive := ParAll(slice, func(i int) bool { return i > 0 }) + assert.False(t, allPositive) + }) + + t.Run("Return true on nil slice", func(t *testing.T) { + var slice []int = nil + allPositive := ParAll(slice, func(i int) bool { return i > 0 }) + assert.True(t, allPositive) + }) +} + +func TestParAny(t *testing.T) { + t.Run("Some elements evaluate to true", func(t *testing.T) { + slice := []int{-1, -4, 6, -2, 3, 7} + anyPositive := ParAny(slice, func(i int) bool { return i > 0 }) + assert.True(t, anyPositive) + }) + + t.Run("All elements evaluate to false", func(t *testing.T) { + slice := []int{-1, -4, -6, -2, -3, -7} + anyPositive := ParAny(slice, func(i int) bool { return i > 0 }) + assert.False(t, anyPositive) + }) + + t.Run("An element evaluates to true on large array", func(t *testing.T) { + slice := Generate(1000, func(idx int) int { return idx + 1 }) + slice[500] = 0 + + anyPositive := ParAny(slice, func(i int) bool { return i == 0 }) + assert.True(t, anyPositive) + }) + + t.Run("Return false on nil slice", func(t *testing.T) { + var slice []int = nil + anyPositive := ParAny(slice, func(i int) bool { return i > 0 }) + assert.False(t, anyPositive) + }) +} + func TestParMap(t *testing.T) { t.Run("Increment int values by one in large array", func(t *testing.T) { slice := Generate(1000, func(idx int) int { return idx }) From bb5bdd79c016c553d2226a0f8fc207262a052041 Mon Sep 17 00:00:00 2001 From: Elias Kauppi Date: Mon, 17 Oct 2022 19:28:08 +0300 Subject: [PATCH 2/2] Update Go version from 1.18 to 1.19. --- .github/workflows/go.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 5fb5b26..3de3817 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -11,7 +11,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v3 with: - go-version: 1.18 + go-version: 1.19 - name: Build run: go build -v ./...