Skip to content

Conversation

@Kamirus
Copy link
Member

@Kamirus Kamirus commented Mar 17, 2025

Adds pure/RealTimeQueue module which implements "Real-Time Double-Ended Queue" (formalization code archive) to achieve worst-case O(1) const-time&space push/pop/peek operations.
This implementation avoids performance cliffs that were leading to unexpected cycle/fuel exhaustion.
However, this implementation has worse amortized performance than pure/Queue. The differences between all three queue implementations are demonstrated in the Queues.bench.mo benchmark.

Resolves #149

TODOs:

  • pure/RealTimeQueue should be an alternative to the pure/Queue, match the following aspects:
    • examples in docs
    • semantics of operations (order of visiting elements should match the pure/Queue) - exception: filter and filterMap preserve the front-to-back order, while pure/Queue does not
    • both should provide the same operations
  • Make a simple benchmark that is fast and illustrates the differences in perf between both immutable and the mutable queues

@Kamirus Kamirus marked this pull request as ready for review April 30, 2025 14:15
@Kamirus Kamirus changed the title Real-time pure/Queue with const time operations Real-time immutable queue with const time operations Apr 30, 2025
@Kamirus Kamirus added the enhancement New feature or request label Apr 30, 2025
@@ -0,0 +1,1026 @@
/// Double-ended immutable queue with guaranteed `O(1)` push/pop operations (caveat: high constant factor).
Copy link
Contributor

Choose a reason for hiding this comment

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

Very nice doc!

/// Space: `O(size)` as the current implementation uses `values` to iterate over the queue.
///
/// *Runtime and space assumes that `f` runs in O(1) time and space.
/// *Runtime and space assumes that the `predicate` runs in `O(1)` time and space.
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if we should make both Queue.any and Queue.all not preserve order, for efficiency and consistency with RealTimeQueue (or vice versa). Queue.contains doesn't either.

Copy link
Member Author

Choose a reason for hiding this comment

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

We could do that, but ideally in another PR, because I know that some might prefer other approach.
Right now, every non-preserving operation in RealTimeQueue is also not preserving in the original pure/Queue. Only filter and filterMap preserve the order here, but not in pure/Queue. This is because of implementation difficulties (not worth to work on that if we might make all operations order preserving in the future)

/// - `#idles`: the queue is in the idle state, where `l` and `r` are non-empty stacks of elements fulfilling the size invariant
/// - `#rebal`: the queue is in the rebalancing state
public type Queue<T> = {
#empty;
Copy link
Contributor

Choose a reason for hiding this comment

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

https://dl.acm.org/doi/pdf/10.1145/165180.165225 actually doesn't distinguish the #empty...#three cases and just uses a finite list. I wonder if that might improve perf a little, but I guess these are rare cases...
Perhaps Nipkow did this so simplify his proof.

Copy link
Member Author

Choose a reason for hiding this comment

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

I've followed this implementation. The paper was not accurate enough.

case (#two(_, _)) 2;
case (#three(_, _, _)) 3;
case (#idles((l, nL), (r, nR))) {
debug assert Stacks.size(l) == nL and Stacks.size(r) == nR;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
debug assert Stacks.size(l) == nL and Stacks.size(r) == nR;
debug assert Stacks.size(l) == nL and Stacks.size(r) == nR;

I wonder if we should disable this assert since peope are probably all building with debug on.

Copy link
Contributor

Choose a reason for hiding this comment

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

(its not 0(1), right)?

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe you could check it in the tests instead?

Copy link
Contributor

@crusso crusso left a comment

Choose a reason for hiding this comment

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

Ok, I think I've done all I can without understanding the actual algorithm!
Very nice work!

@crusso
Copy link
Contributor

crusso commented May 2, 2025

I'll leave it up to you whether you want to apply the microoptimization in #307 or not (about 9-10% on bench)

@Kamirus
Copy link
Member Author

Kamirus commented May 2, 2025

@crusso I've applied the microoptimization + I've inlined reverse inside push/popBack and got -20% savings

Instructions

pure/Queue pure/RealTimeQueue mutable Queue
Initialize with 2 elements 3_571 (0%) 2_592 (-5.47%) 3_401 (0%)
Push 500 elements 103_492 (0%) 867_120 (-18.16%) 243_585 (0%)
Pop front 2 elements 98_792 (0%) 5_009 (-4.46%) 4_326 (0%)
Pop 150 front&back 106_545 (0%) 350_417 (-20.42%) 140_211 (0%)

Garbage Collection

pure/Queue pure/RealTimeQueue mutable Queue
Initialize with 2 elements 508 B (0%) 444 B (-5.93%) 456 B (0%)
Push 500 elements 10.1 KiB (0%) 137.84 KiB (-21.80%) 344 B (0%)
Pop front 2 elements 12.19 KiB (0%) 528 B (-10.81%) 424 B (0%)
Pop 150 front&back 15.61 KiB (0%) 49.66 KiB (-27.32%) 12.1 KiB (0%)

Copy link
Collaborator

@rvanasa rvanasa left a comment

Choose a reason for hiding this comment

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

Thanks @crusso for the in-depth review! Thinking that we could merge this and make follow-up changes in separate PRs if needed.

@Kamirus Kamirus merged commit 7a66568 into main May 12, 2025
15 checks passed
@Kamirus Kamirus deleted the kamil/deque branch May 12, 2025 09:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

(pure) Queue is an amortized queue. Nipkow has non-amortized functional queue that may serve us better

3 participants