Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
d97cb2c
types + some funcs
Kamirus Mar 5, 2025
847bbab
Merge branch 'main' into kamil/deque
Kamirus Mar 5, 2025
aa66dbe
bigstate, smallstate, commonstate
Kamirus Mar 6, 2025
5826b9e
missing small state functions + states
Kamirus Mar 6, 2025
f1ba61a
pushFront and Back
Kamirus Mar 6, 2025
1eaf4b3
pop operations
Kamirus Mar 6, 2025
f7cab44
Merge branch 'main' into kamil/deque
Kamirus Mar 6, 2025
af4e79b
fix missing List.push after merge
Kamirus Mar 6, 2025
d05beb1
empty, isEmpty, singleton, size. Also: docs, refactor, fixes
Kamirus Mar 7, 2025
da12689
stacks as module
Kamirus Mar 7, 2025
a818404
current as module
Kamirus Mar 7, 2025
13d747d
refactor
Kamirus Mar 7, 2025
610f44e
docs, refactors and pre-contains
Kamirus Mar 7, 2025
6a2236b
first tests, fix popBack signature
Kamirus Mar 10, 2025
460503e
remove wrong assert
Kamirus Mar 10, 2025
5b4aaaf
almost all tests migrated
Kamirus Mar 10, 2025
20205b8
missing operations
Kamirus Mar 10, 2025
b366ebf
whole test suite ported
Kamirus Mar 11, 2025
d756b15
all operations done + refactor
Kamirus Mar 11, 2025
7c203a5
test missing functions
Kamirus Mar 11, 2025
8b04421
more code coverage
Kamirus Mar 11, 2025
2908292
more coverage
Kamirus Mar 11, 2025
5503764
add reverse test
Kamirus Mar 11, 2025
df049d6
more edge case tests
Kamirus Mar 11, 2025
380f3c8
debug size invariants
Kamirus Mar 14, 2025
6a03bad
remove done todos
Kamirus Mar 14, 2025
7effbae
peek without pop
Kamirus Mar 14, 2025
7e45ba6
documentation
Kamirus Mar 14, 2025
c996edf
improve docs
Kamirus Mar 14, 2025
d1d56ab
arbitrary order in operations for limiting allocations
Kamirus Mar 17, 2025
7926e67
valuesRev
Kamirus Mar 17, 2025
94e62f6
remove todo
Kamirus Mar 17, 2025
e0824ab
validation api & cleanup
Kamirus Mar 17, 2025
a2f2c81
Merge branch 'main' into kamil/deque
Kamirus Mar 17, 2025
50e32a7
remove the old pure Queue
Kamirus Mar 18, 2025
8037ae5
rename Deque to Queue without file name change yet
Kamirus Mar 18, 2025
c515cee
rename modules
Kamirus Mar 18, 2025
1e66d31
add compare, fix api, tests
Kamirus Mar 18, 2025
c857fc5
add regression test
Kamirus Mar 19, 2025
5e16e77
Merge branch 'main' into kamil/deque
Kamirus Mar 24, 2025
60b6680
restore the old pure queue
Kamirus Apr 28, 2025
5fa7755
Merge branch 'main' into kamil/deque
Kamirus Apr 28, 2025
5d5cb00
review + doc
Kamirus Apr 29, 2025
0dc5733
doc example changes
Kamirus Apr 29, 2025
5b41e4b
implement reverse for the pure/Queue, use in tests, fix tests
Kamirus Apr 29, 2025
fd47aa2
match the api for both pure queues, remove valuesRev
Kamirus Apr 29, 2025
aca15d1
fix api, just parameter names
Kamirus Apr 29, 2025
a7d6ea8
fix examples
Kamirus Apr 29, 2025
8493fa7
Merge branch 'main' into kamil/deque
Kamirus Apr 29, 2025
5fe64a8
match the visiting order to the PureQueue
Kamirus Apr 30, 2025
e414759
benchmark
Kamirus Apr 30, 2025
a2faef2
changelog
Kamirus Apr 30, 2025
5cb01b6
fix description for CI
Kamirus Apr 30, 2025
f22ae48
fix toText and remove debug print from tests
Kamirus Apr 30, 2025
6c73ce6
improve description
Kamirus Apr 30, 2025
f823fdb
fix description
Kamirus Apr 30, 2025
f2ea009
docs and pattern optimization
Kamirus May 2, 2025
7caf0c7
log
crusso May 2, 2025
a66aed5
reorder test and reduce allcoations
crusso May 2, 2025
898ef8b
avoid another allocation
crusso May 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## Next

* Add `reverse` function to the `pure/Queue` module (#229).
* Add `pure/RealTimeQueue` module - an alternative immutable double-ended queue implementation with worst-case `O(1)` time complexity for all operations but worse amortized performance (#229).
- Refer to the `Queues.bench.mo` benchmark for comparison with other queue implementations.
* Rename `Map.replaceIfExists()` to `Map.replace()` (#286).
* Add `entriesFrom` and `reverseEntriesFrom` to `Map`, `valuesFrom` and `reverseValuesFrom` to `Set` and `Text.toText` (#272).
* Update code examples in doc comments (#224, #282).
Expand Down
108 changes: 108 additions & 0 deletions bench/Queues.bench.mo
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import Bench "mo:bench";

import Array "../src/Array";
import Nat "../src/Nat";
import Option "../src/Option";
import OldQueue "../src/pure/Queue";
import NewQueue "../src/pure/RealTimeQueue";
import MutQueue "../src/Queue";
import Runtime "../src/Runtime";

module {
public func init() : Bench.Bench {
let bench = Bench.Bench();

bench.name("Different queue implementations");
bench.description("Compare the performance of the following queue implementations_:
- `pure/Queue`: The default immutable double-ended queue implementation.
* Pros: Good amortized performance, meaning that the average cost of operations is low `O(1)`.
* Cons: In worst case, an operation can take `O(size)` time rebuilding the queue as demonstrated in the `Pop front 2 elements` scenario.
- `pure/RealTimeQueue`
* Pros: Every operation is guaranteed to take at most `O(1)` time and space.
* Cons: Poor amortized performance: Instruction cost is on average 4x for *pop* and 10x for *push* compared to `pure/Queue`.
- mutable `Queue`
* Pros: Also `O(1)` guarantees with a lower constant factor than `pure/RealTimeQueue`. Amortized performance is comparable to `pure/Queue`.
* Cons: It is mutable and cannot be used in `shared` types (not shareable)_.");

bench.rows([
"Initialize with 2 elements",
"Push 500 elements",
"Pop front 2 elements",
"Pop 150 front&back"
]);
bench.cols([
"pure/Queue",
"pure/RealTimeQueue",
"mutable Queue"
]);

let init = Array.repeat(1, 2);
var newQ = NewQueue.empty<Nat>();
var oldQ = OldQueue.empty<Nat>();
var mutQ = MutQueue.empty<Nat>();

let toPush = Array.repeat(7, 500);

bench.runner(
func(row, col) = switch (col, row) {
case ("pure/RealTimeQueue", "Initialize with 2 elements") newQ := NewQueue.fromIter<Nat>(init.vals());
case ("pure/Queue", "Initialize with 2 elements") oldQ := OldQueue.fromIter<Nat>(init.vals());
case ("mutable Queue", "Initialize with 2 elements") mutQ := MutQueue.fromIter<Nat>(init.vals());
case ("pure/RealTimeQueue", "Push 500 elements") {
for (i in toPush.vals()) {
newQ := NewQueue.pushBack<Nat>(newQ, i)
}
};
case ("pure/Queue", "Push 500 elements") {
for (i in toPush.vals()) {
oldQ := OldQueue.pushBack<Nat>(oldQ, i)
}
};
case ("mutable Queue", "Push 500 elements") {
for (i in toPush.vals()) {
MutQueue.pushBack<Nat>(mutQ, i)
}
};
case ("pure/RealTimeQueue", "Pop front 2 elements") Option.unwrap(
do ? {
newQ := NewQueue.popFront<Nat>(NewQueue.popFront<Nat>(newQ)!.1)!.1
}
);
case ("pure/Queue", "Pop front 2 elements") Option.unwrap(
do ? {
oldQ := OldQueue.popFront<Nat>(OldQueue.popFront<Nat>(oldQ)!.1)!.1
}
);
case ("mutable Queue", "Pop front 2 elements") Option.unwrap(
do ? {
ignore MutQueue.popFront<Nat>(mutQ);
ignore MutQueue.popFront<Nat>(mutQ)
}
);
case ("pure/RealTimeQueue", "Pop 150 front&back") {
for (i in Nat.range(0, 150)) Option.unwrap(
do ? {
newQ := NewQueue.popBack<Nat>(NewQueue.popFront<Nat>(newQ)!.1)!.0
}
)
};
case ("pure/Queue", "Pop 150 front&back") {
for (i in Nat.range(0, 150)) Option.unwrap(
do ? {
oldQ := OldQueue.popBack<Nat>(OldQueue.popFront<Nat>(oldQ)!.1)!.0
}
)
};
case ("mutable Queue", "Pop 150 front&back") {
for (i in Nat.range(0, 150)) {
ignore MutQueue.popFront<Nat>(mutQ);
ignore MutQueue.popBack<Nat>(mutQ)
}
};
case _ Runtime.unreachable()
}
);

bench
}
}
63 changes: 63 additions & 0 deletions log
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@

> @dfinity/[email protected] bench
> mops bench Queues

Benchmark files:
• bench/Queues.bench.mo

==================================================

Starting dfx replica...
Deploying canisters...

--------------------------------------------------

Running bench/Queues.bench.mo...



Different queue implementations

Compare the performance of the following queue implementations_:
- `pure/Queue`: The default immutable double-ended queue implementation.
* Pros: Good amortized performance, meaning that the average cost of operations is low `O(1)`.
* Cons: In worst case, an operation can take `O(size)` time rebuilding the queue as demonstrated in the `Pop front 2 elements` scenario.
- `pure/RealTimeQueue`
* Pros: Every operation is guaranteed to take at most `O(1)` time and space.
* Cons: Poor amortized performance: Instruction cost is on average 4x for *pop* and 10x for *push* compared to `pure/Queue`.
- mutable `Queue`
* Pros: Also `O(1)` guarantees with a lower constant factor than `pure/RealTimeQueue`. Amortized performance is comparable to `pure/Queue`.
* Cons: It is mutable and cannot be used in `shared` types (not shareable)_.


Instructions

| | pure/Queue | pure/RealTimeQueue | mutable Queue |
| :------------------------- | ---------: | -----------------: | ------------: |
| Initialize with 2 elements | 3_571 | 2_934 | 3_401 |
| Push 500 elements | 103_492 | 966_398 | 243_585 |
| Pop front 2 elements | 98_792 | 5_009 | 4_326 |
| Pop 150 front&back | 106_545 | 394_667 | 140_211 |


Heap

| | pure/Queue | pure/RealTimeQueue | mutable Queue |
| :------------------------- | ---------: | -----------------: | ------------: |
| Initialize with 2 elements | 324 B | 300 B | 352 B |
| Push 500 elements | 8.08 KiB | 8.17 KiB | 19.8 KiB |
| Pop front 2 elements | 240 B | 240 B | 192 B |
| Pop 150 front&back | -4.42 KiB | -492 B | -11.45 KiB |


Garbage Collection

| | pure/Queue | pure/RealTimeQueue | mutable Queue |
| :------------------------- | ---------: | -----------------: | ------------: |
| Initialize with 2 elements | 508 B | 472 B | 456 B |
| Push 500 elements | 10.1 KiB | 166.63 KiB | 344 B |
| Pop front 2 elements | 12.19 KiB | 528 B | 424 B |
| Pop 150 front&back | 15.61 KiB | 60.38 KiB | 12.1 KiB |


Stopping replica...
81 changes: 53 additions & 28 deletions src/pure/Queue.mo
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
///
/// `n` denotes the number of elements stored in the queue.
///
/// Note that some operations that traverse the elements of the queue (e.g. `forEach`, `values`) preserve the order of the elements,
/// whereas others (e.g. `map`, `contains`) do NOT guarantee that the elements are visited in any order.
/// The order is undefined to avoid allocations, making these operations more efficient.
///
/// ```motoko name=import
/// import Queue "mo:base/pure/Queue";
/// ```
Expand Down Expand Up @@ -115,9 +119,9 @@ module {
/// }
/// ```
///
/// Runtime: O(size)
/// Runtime: `O(size)`
///
/// Space: O(1)
/// Space: `O(1)`
public func contains<T>(queue : Queue<T>, equal : (T, T) -> Bool, item : T) : Bool = List.contains(queue.0, equal, item) or List.contains(queue.2, equal, item);

/// Inspect the optional element on the front end of a queue.
Expand Down Expand Up @@ -370,11 +374,11 @@ module {
/// }
/// ```
///
/// Runtime: O(size)
/// Runtime: `O(size)`
///
/// Space: O(size) as the current implementation uses `values` to iterate over the queue.
/// 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.
public func all<T>(queue : Queue<T>, predicate : T -> Bool) : Bool {
for (item in values queue) if (not (predicate item)) return false;
return true
Expand All @@ -392,11 +396,11 @@ module {
/// }
/// ```
///
/// Runtime: O(size)
/// Runtime: `O(size)`
///
/// Space: O(size) as the current implementation uses `values` to iterate over the queue.
/// 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.
public func any<T>(queue : Queue<T>, predicate : T -> Bool) : Bool {
for (item in values queue) if (predicate item) return true;
return false
Expand All @@ -415,17 +419,18 @@ module {
/// }
/// ```
///
/// Runtime: O(size)
/// Runtime: `O(size)`
///
/// Space: O(size)
/// Space: `O(size)`
///
/// *Runtime and space assumes that `f` runs in O(1) time and space.
/// *Runtime and space assumes that `f` runs in `O(1)` time and space.
public func forEach<T>(queue : Queue<T>, f : T -> ()) = for (item in values queue) f item;

/// Call the given function `f` on each queue element and collect the results
/// in a new queue.
///
/// Note: The order of visiting elements is undefined with the current implementation.
///
/// Example:
/// ```motoko include=import
/// import Iter "mo:base/Iter";
Expand All @@ -438,10 +443,11 @@ module {
/// }
/// ```
///
/// Runtime: O(size)
/// Runtime: `O(size)`
///
/// Space: O(size)
/// *Runtime and space assumes that `f` runs in O(1) time and space.
/// Space: `O(size)`
///
/// *Runtime and space assumes that `f` runs in `O(1)` time and space.
public func map<T1, T2>(queue : Queue<T1>, f : T1 -> T2) : Queue<T2> {
let (fr, n, b) = queue;
(List.map(fr, f), n, List.map(b, f))
Expand All @@ -461,13 +467,15 @@ module {
/// }
/// ```
///
/// Runtime: O(size)
/// Runtime: `O(size)`
///
/// Space: O(size)
public func filter<T>(queue : Queue<T>, f : T -> Bool) : Queue<T> {
/// Space: `O(size)`
///
/// *Runtime and space assumes that `predicate` runs in `O(1)` time and space.
public func filter<T>(queue : Queue<T>, predicate : T -> Bool) : Queue<T> {
let (fr, _, b) = queue;
let front = List.filter(fr, f);
let back = List.filter(b, f);
let front = List.filter(fr, predicate);
let back = List.filter(b, predicate);
check(front, List.size front + List.size back, back)
};

Expand All @@ -488,11 +496,11 @@ module {
/// }
/// ```
///
/// Runtime: O(size)
/// Runtime: `O(size)`
///
/// Space: O(size)
/// Space: `O(size)`
///
/// *Runtime and space assumes that `f` runs in O(1) time and space.
/// *Runtime and space assumes that `f` runs in `O(1)` time and space.
public func filterMap<T, U>(queue : Queue<T>, f : T -> ?U) : Queue<U> {
let (fr, _n, b) = queue;
let front = List.filterMap(fr, f);
Expand All @@ -513,9 +521,9 @@ module {
/// }
/// ```
///
/// Runtime: O(size)
/// Runtime: `O(size)`
///
/// Space: O(size)
/// Space: `O(size)`
public func toText<T>(queue : Queue<T>, f : T -> Text) : Text {
var text = "PureQueue[";
func add(item : T) {
Expand All @@ -540,11 +548,11 @@ module {
/// }
/// ```
///
/// Runtime: O(size)
/// Runtime: `O(size)`
///
/// Space: O(size)
/// Space: `O(size)`
///
/// *Runtime and space assumes that argument `compare` runs in O(1) time and space.
/// *Runtime and space assumes that argument `compareItem` runs in `O(1)` time and space.
public func compare<T>(queue1 : Queue<T>, queue2 : Queue<T>, compareItem : (T, T) -> Order.Order) : Order.Order {
let (i1, i2) = (values queue1, values queue2);
loop switch (i1.next(), i2.next()) {
Expand All @@ -556,6 +564,23 @@ module {
case (null, _) return #less;
case (_, null) return #greater
}
}
};

/// Reverse the order of elements in a queue.
/// This operation is cheap, it does NOT require copying the elements.
///
/// Example:
/// ```motoko include=import
/// persistent actor {
/// let queue = Queue.fromIter([1, 2, 3].values());
/// let reversed = Queue.reverse(queue);
/// assert Queue.peekFront(reversed) == ?3;
/// assert Queue.peekBack(reversed) == ?1;
/// }
/// ```
///
/// Runtime: `O(1)`
///
/// Space: `O(1)`
public func reverse<T>(queue : Queue<T>) : Queue<T> = (queue.2, queue.1, queue.0)
}
Loading
Loading