Skip to content

Optimize Mutable.nextPermutation and add {next/prev}permutation(By) - #498

Merged
Shimuuar merged 7 commits into
haskell:masterfrom
gksato:optimize-nextperm
Aug 19, 2024
Merged

Optimize Mutable.nextPermutation and add {next/prev}permutation(By)#498
Shimuuar merged 7 commits into
haskell:masterfrom
gksato:optimize-nextperm

Conversation

@gksato

@gksatogksato commented Jul 21, 2024

Copy link
Copy Markdown
Contributor

This implements some optimization of nextPermutation from Data.Vector.Generic.Mutable, and supercedes #497. The main content of this re-implementation is the following two points:

  1. Wrapping the whole implementation in stToPrim. This allows the compiler to optimize the code better.
  2. When finding the rightmost increasing pair v[k]<v[k+1], we now search from the right, instead of from the left. This allows us to abort the search as soon as we find such a pair, giving average-case constant performance, instead of best-case linear in the previous implementation.

Also, this adds some clarification on doc comments; nextPermutation does not update the source vector when the given vector is the last permutation.


Update

This also adds the following API:

Data.Vector.*.Mutable.nextPermutationBy ::Constraint=> (e->e->Ordering) ->v (PrimStatem) a->mBoolData.Vector.*.Mutable.prevPermutation :: (Constraint, Orde) =>v (PrimStatem) a->mBoolData.Vector.*.Mutable.prevPermutationBy ::Constraint=> (e->e->Ordering) ->v (PrimStatem) a->mBool

@toyboot4etoyboot4e mentioned this pull request Jul 21, 2024
@Shimuuar

Copy link
Copy Markdown
Contributor

Thanks!

I'll look into this tomorrow but at a glance nextPermutationByLt miss INLINE pragma which means it won't be specialized. Is this intentional?

@gksato

Copy link
Copy Markdown
ContributorAuthor

I just aligned it with the original implementation; I assumed it was intentional. Is it better to add it before your review?

@Shimuuar

Copy link
Copy Markdown
Contributor

I just aligned it with the original implementation

I see. I missed that somehow.

Is it better to add it before your review?

No need. I'll benchmark it and going to tweak code a bit anyway

@Shimuuar

Copy link
Copy Markdown
Contributor

Yes. This is an optimization. I used very simple benchmark

permute::Int->Int->IO (VectorInt)
permute sz n_it =do
vec <-MV.generate sz id
replicateM_ n_it $MV.nextPermutation vec
V.unsafeFreeze vec

With vector size 20 and n_it varying in range 1000-10000. See Shimuuar/vector branch bench-next-permutation for very quick and dirty code.

Benchmarks

I've run five benchmarks:

  1. [00.Base] Baseline from current master
  2. [01.strict] Added strictness. First change from make nextPermutation faster #497
  3. [02.stToPrim] Added stToPrim
  4. [03.INLINE] Added INLINE pragma
  5. [04.gksato] Version from this PR

image

So for unboxed vector of ints stToPrim gives ~4× runtime speedup and your optimization another 3×. Of course inlined and specialized version beats them all.

Do you plan to include functions proposed in #499 in this PR?

@gksato

Copy link
Copy Markdown
ContributorAuthor

Do you plan to include functions proposed in #499 in this PR?

I was not sure if it was appropriate to put optimization and API addition in one PR, but I'm happy to do it if you propose so! Should CHANGELOG be added now?

gksato added 3 commits July 27, 2024 14:51
Clarified that `Data.Vector.*.Mutable.nextPermutation` does not
update the vector when the original state is the last permutation.
This implements some optimization of `nextPermutation` from
`Data.Vector.Generic.Mutable`. The main content of this
re-implementation is the following two points:
1. Wrapping the whole implementation in `stToPrim`. This
allows the compiler to optimize the code better.
2. When finding the rightmost increasing pair v[k]<v[k+1], we now
search from the right, instead of from the left. This allows us to
abort the search as soon as we find such a pair, giving
average-case constant performance, instead of best-case linear
in the previous implementation.
This adds the following three companions to the already existing
`Data.Vector.*.Mutable.nextPermutation`:
- `Data.Vector.*.Mutable.nextPermutationBy`
- `Data.Vector.*.Mutable.prevPermutation`
- `Data.Vector.*.Mutable.prevPermutationBy`
They are all implemented in terms of the already existing
`Data.Vector.Generic.Mutable.nextPermutationLt`.
@gksato

gksato commented Jul 27, 2024

Copy link
Copy Markdown
ContributorAuthor

Since I forgot to tweak the comments in Data.Vector.*.Mutablein 4ac750f, I'm going to force-push an updated history.

The function `Data.Vector.Generic.Mutable.nextPermutationByLt` is
the unified implementation for the family of functions
`Data.Vector.*.Mutable.{next,prev}Permutation{,By}`.
By adding INLINE pragma to it, we may expect the some performance gain
from specialization.
@gksato
gksatoforce-pushed the optimize-nextperm branch from 272fa34 to 063c8efCompareJuly 27, 2024 07:24
@gksato

gksato commented Jul 27, 2024

Copy link
Copy Markdown
ContributorAuthor

I added INLINE to the internal routine nextPermutationByLt, because cabal v2-bench with a line of bench "permute" $ whnfIO 20 useSize proved it 10x faster with INLINE (I haven't tweaked vector-bench-papi, because I'm on MacOS).

@gksatogksato changed the title Optimize Data.Vector.Generic.Mutable.nextPermutationOptimize Mutable.nextPermutation and add {next/prev}permutation(By)Jul 27, 2024
@gksato

Copy link
Copy Markdown
ContributorAuthor

Oh, and I added:

Data.Vector.*.Mutable.nextPermutationBy ::Constraint=> (e->e->Ordering) ->v (PrimStatem) a->mBoolData.Vector.*.Mutable.prevPermutation :: (Constraint, Orde) =>v (PrimStatem) a->mBoolData.Vector.*.Mutable.prevPermutationBy ::Constraint=> (e->e->Ordering) ->v (PrimStatem) a->mBool

The TODOs we have now are... tests, benchmarks, and changelog? Are they all?

This adds the following three tests for the pair of functions
`Data.Vector.*.Mutable.{next/prev}Permutation` in
`vector/tests/Tests/Move.hs`:
1. `testRevPermutations`: For n=1,..,7, repeatedly applying
`prevPermutation` to a vector `[n,n-1..1]` produces all n! permutations
of the vector in reverse order, and applying the function to the
lexicographically smallest permutation doesn't change the vector.
2. `testNPPermutationsIsId`: Applying `nextPermutation` followed by
`prevPermutation` to a vector produces the original vector.
Note that this function uses modified versions of `nextPermutation`
and `prevPermutation` that reverse the vector if the original function
returns `False`, rendering those two functions bijective.
3. `testPNPermutationsIsId`: Applying `prevPermutation` followed by
`nextPermutation` to a vector produces the original vector. The same
caveat as above applies here.
@gksato

gksato commented Aug 3, 2024

Copy link
Copy Markdown
ContributorAuthor

I've written some tests on a separate branch (gksato:optimize-nextperm-tidyup), since I don't know if it is wanted. I'd fast-forward the branch (gksato:optimize-nextperm) of this pull request to include that commit whenever any of you request it!

@Shimuuar

Copy link
Copy Markdown
Contributor

I finally found time to review PR. Please add tests and this PR is good to go. It would be nice to add changelog entry as well and whether you want to add benchmarks or not is up to you.

@gksato

gksato commented Aug 17, 2024

Copy link
Copy Markdown
ContributorAuthor

@Shimuuar OK, I'll do that soon. I've been writing some benchmark, so I might add it if implementation doesn't take much time. For the purpose of the changelog and @since pragma, what should be the version? I assume 0.13.2.0, but I'd like to be sure that it'll not be 0.13.1.1?

@Shimuuar

Copy link
Copy Markdown
Contributor

Yes 0.13.2.0. Adding new definitions to module requires at least minor bump according to PVP. Patch releases are for bugfixes and documentation

This adds a changelog entry and `@since` annotations for:
- Optimization of `Data.Vector.Generic.Mutable.nextPermutation`
- Addition of `Data.Vector.Generic.Mutable.prevPermutation(By)`
- Addition of `Data.Vector.Generic.Mutable.nextPermutationBy`
This also tweaks the haddock comments of the functions for
`Data.Vector.*.Mutable.(next|prev)Permutation(By)?` for a better
readability.
Implement benchmarks to test performance of nextPermutation and
prevPermutation on mutable vectors. Tests include:
- Looping through all permutations on small vectors
- Applying bijective versions n times on:
- Ascending permutations of size n
- Descending permutations of size n
- Random permutations of size n
- For a baseline, copying a vector of size n once. Benchmarks for
bijective permutations begins with such a copy, and you might want
to remove the impact of copying from the results.
Benchmarks cover both forward (next) and reverse (prev) operations.
@gksato

Copy link
Copy Markdown
ContributorAuthor

Added tests, a changelog entry, @since annotations, and benchmarks. Force-pushed some commit message rewording because @since without quotes refers to someone's name on a GitHub!

Note that vector-bench-papi hasn't been updated, because they say it doesn't go nicely on MacOS (I haven't tried it myself). However, I assume that applying the same diff as vector/benchmarks/Main.hs to vector-bench-papi/benchmarks/Main.hs should work.

@Shimuuar
Shimuuar merged commit eb60526 into haskell:masterAug 19, 2024
@Shimuuar

Copy link
Copy Markdown
Contributor

That's quite formidable battery of tests all right! It's more thorough than rest of benchmark suite :)

I'm unhappy with inlining of worker function. It's too large. But we're running into GHC's limitations since we don't way to ask GHC to generate specializations for it and its callers automatically.

Excellent pull request. Thank you!

Note that vector-bench-papi hasn't been updated

Yes ..-papi package should modified same way. And that's the point when you start to want to use backpack in order to be able to swap bencmarking engine. I implemented poor man's version in math-functions. Sadly backpack never caught on

P.S. @since. That poor guy. He must hate haskell.

@gksato

Copy link
Copy Markdown
ContributorAuthor

I appreciate your work on review and merge, thank you!

Oh, yes. I haven't noticed this would of course be a nice use case for backpack. I imagined what if we try to comply DRY without backpack... urgh, it's a mess!

Sadly backpack never caught on

I truly agree with you.

@gksato

Copy link
Copy Markdown
ContributorAuthor

I've noticed that the new version is on its way, which I'm glad to see. I appreciate your work.
However, I would like to poke you about a small, minute detail:

Note that vector-bench-papi hasn't been updated

Yes ..-papi package should modified same way.

Can we leave it as it is? I was just too lazy to build a Docker container to run vector-bench-papi on my MacOS and I didn't modify it in this PR.

@Shimuuar

Copy link
Copy Markdown
Contributor

Thanks for reminding me. I totally forgot. I'll fix it after making release. It's fine since vector-bench-papi is not published on hackage anyway

@gksato

Copy link
Copy Markdown
ContributorAuthor

I see, I leave it to you! Thanks for your good maintaining work!

netbsd-srcmastr pushed a commit to NetBSD/pkgsrc that referenced this pull request Jan 29, 2025
# Changes in version 0.13.2.0
* Strict boxed vector `Data.Vector.Strict` and `Data.Vector.Strict.Mutable` is
added (#488). it ensures that all values in the vector are evaluated to WHNF.
* `DoNotUnboxStrict`, `DoNotUnboxLazy`, and `DoNotUnboxNormalForm` wrapper are
added for defining unbox instances for types that contain not unboxable fields.
[#503](haskell/vector#506),
[#508](haskell/vector#508)
* `spanR` and `breakR` were added [#476](haskell/vector#476).
They allow parsing vector from the right.
* We had some improvements on `*.Mutable.{next,prev}Permutation{,By}`
[#498](haskell/vector#498):
* Add `*.Mutable.prevPermutation{,By}` and `*.Mutable.nextPermutationBy`
* Improve time performance. We may now expect good specialization supported by inlining.
The implementation has also been algorithmically updated: in the previous implementation
the full enumeration of all the permutations of `[1..n]` took Omega(n*n!), but it now takes O(n!).
* Add tests for `{next,prev}Permutation`
* Add benchmarks for `{next,prev}Permutation`
* Cabal >= 3.0 is now required for building package (#481).
* `vector:benchmarks-O2` public sublibrary containing benchmarks is added (#481).
* Type family `Mutable` provides instances for arrays from `primitive`.
* Various documentation improvements.
msk pushed a commit to msk/pkgsrc that referenced this pull request May 11, 2026
# Changes in version 0.13.2.0
* Strict boxed vector `Data.Vector.Strict` and `Data.Vector.Strict.Mutable` is
added (#488). it ensures that all values in the vector are evaluated to WHNF.
* `DoNotUnboxStrict`, `DoNotUnboxLazy`, and `DoNotUnboxNormalForm` wrapper are
added for defining unbox instances for types that contain not unboxable fields.
[#503](haskell/vector#506),
[#508](haskell/vector#508)
* `spanR` and `breakR` were added [#476](haskell/vector#476).
They allow parsing vector from the right.
* We had some improvements on `*.Mutable.{next,prev}Permutation{,By}`
[#498](haskell/vector#498):
* Add `*.Mutable.prevPermutation{,By}` and `*.Mutable.nextPermutationBy`
* Improve time performance. We may now expect good specialization supported by inlining.
The implementation has also been algorithmically updated: in the previous implementation
the full enumeration of all the permutations of `[1..n]` took Omega(n*n!), but it now takes O(n!).
* Add tests for `{next,prev}Permutation`
* Add benchmarks for `{next,prev}Permutation`
* Cabal >= 3.0 is now required for building package (#481).
* `vector:benchmarks-O2` public sublibrary containing benchmarks is added (#481).
* Type family `Mutable` provides instances for arrays from `primitive`.
* Various documentation improvements.
jperkin pushed a commit to TritonDataCenter/pkgsrc that referenced this pull request May 14, 2026
# Changes in version 0.13.2.0
* Strict boxed vector `Data.Vector.Strict` and `Data.Vector.Strict.Mutable` is
added (#488). it ensures that all values in the vector are evaluated to WHNF.
* `DoNotUnboxStrict`, `DoNotUnboxLazy`, and `DoNotUnboxNormalForm` wrapper are
added for defining unbox instances for types that contain not unboxable fields.
[#503](haskell/vector#506),
[#508](haskell/vector#508)
* `spanR` and `breakR` were added [#476](haskell/vector#476).
They allow parsing vector from the right.
* We had some improvements on `*.Mutable.{next,prev}Permutation{,By}`
[#498](haskell/vector#498):
* Add `*.Mutable.prevPermutation{,By}` and `*.Mutable.nextPermutationBy`
* Improve time performance. We may now expect good specialization supported by inlining.
The implementation has also been algorithmically updated: in the previous implementation
the full enumeration of all the permutations of `[1..n]` took Omega(n*n!), but it now takes O(n!).
* Add tests for `{next,prev}Permutation`
* Add benchmarks for `{next,prev}Permutation`
* Cabal >= 3.0 is now required for building package (#481).
* `vector:benchmarks-O2` public sublibrary containing benchmarks is added (#481).
* Type family `Mutable` provides instances for arrays from `primitive`.
* Various documentation improvements.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@gksato@Shimuuar