Usage: #include "sequence.h"
An array_sequence<T> is a concrete implementation of the sequence
ADT/concept that maintains a dynamically-sized array. If you are familiar with std::vector
in the C++ standard library, array_sequence is essentially a parallel version of it.
It implements most of the same operations but with span-efficient algorithms instead of purely sequential ones.
Its elements are accessed with
the square bracket operator, just like an array in C or C++'s std::array or
std::vector, so S[i] is the i-th element of
S.
array_sequence is a class template, which means it is parameterized
by an argument that indicates the type of the elements in the sequence. In C++, different
instantiations of the same class template are considered to be different types, so array_sequence<int>,
array_sequence<double>, and array_sequence<array_sequence<int>>
are three different types.
There are four main constructors for array_sequence. Writing just the name
and type with no arguments gives an empty sequence. Empty braces mean the same thing, but empty round brackets do not!
par::array_sequence<int> A; // A is [] (empty list, but still with type int)
par::array_sequence<int> A{}; // same
par::array_sequence<int> A(); // NOT a sequence: this declares a function called A!
Giving a length $n$ gives you that many elements, each set to the default value for the type, which is zero for numbers. This costs $O(n)$ work and $O(1)$ span.
par::array_sequence<int> B(3); // B is [0, 0, 0]
Giving a length and a value fills the sequence with that many copies of the value, again $O(n)$ work and $O(1)$ span.
par::array_sequence<int> C(3, 7); // C is [7, 7, 7]
Listing the elements in braces gives a sequence containing exactly those elements (this is
called an initializer list). This is usually the clearest way to write a short sequence out by hand.
It is optional to put an = between the name and the list*.
par::array_sequence<int> D{1, 2, 3}; // D is [1, 2, 3]
par::array_sequence<int> D = {1, 2, 3}; // same
Whenever the values have a concrete type that the compiler can see, you can leave out the template argument (<int>) and
let the compiler deduce it for you.
par::array_sequence E{1, 2, 3}; // E is an array_sequence<int> = [1, 2, 3]
par::array_sequence F{1.5, 2.5}; // F is an array_sequence<double> = [1.5, 2.5]
par::array_sequence G(3, 7); // G is an array_sequence<int> = [7, 7, 7]
par::array_sequence<int> A(3); // [0, 0, 0] three elements
par::array_sequence<int> B{3}; // [3] one element
par::array_sequence<int> C(10, 2); // [2, 2, ... ] ten elements
par::array_sequence<int> D{10, 2}; // [10, 2] two elements
Round brackets call the corresponding constructor; braces provide an initializer list of elements.
Empty round brackets are worse still: par::array_sequence<int> A(); declares a
function called A that returns a sequence, rather than making one. Use
A or A{}.
tabulate |
Work | Span |
|---|---|---|
|
$\displaystyle\sum_{i=0}^{n-1} \mathcal{W}(f(i))$ | $\displaystyle\max_{i=0}^{n-1} \mathcal{S}(f(i))$ |
tabulate takes a length n and a function f, and creates an array_sequence of length n
such that the i-th element is f(i). The work and span bounds assume that invoking f takes constant time.
Often we will use a lambda as the function f. For example, to create a sequence S of length 1000 such that S[i] == i, i.e., the sequence [0,1,2,...,999], we could write
auto S = par::tabulate(1000, [](int i) { return i; });
The type of S will be array_sequence<int>. Note that we did not have to specify the value type T explicitly, it was deduced from the return type of the lambda (int). If we want
a different type, we can explicitly specify a return type for the lambda:
auto S = par::tabulate(1000, [](int i) -> double { return i; });
to_sequence |
Work | Span |
|---|---|---|
|
$O(|S|)$ | $O(1)$ |
to_sequence converts a value of any sequence type into an array_sequence. The cost bounds assume that each element of the sequence being converted can be materialized in constant time.
For example, we can convert a std::vector into an array_sequence
by writing
std::vector v{1,2,3};
auto a = par::to_sequence(v);
size |
Work | Span |
|---|---|---|
|
$O(1)$ | $O(1)$ |
size returns the number of elements in the sequence.
operator[] |
Work | Span |
|---|---|---|
|
$O(1)$ | $O(1)$ |
S[i] is the i-th element of S, counting from
zero. It can be read from and, unless S is const, assigned
to.
par::array_sequence<int> S{1,2,3};
int x = S[0]; // x == 1
S[2] = 10; // S == [1,2,10]
const par::array_sequence<int> S2{1,2,3};
int y = S2[0]; // y == 1, reading is fine
S2[2] = 10; // does not compile, S2 is const
Two sequences with the same element type can be compared directly. == is
true when they are the same length and their elements are equal. The ordering operators
compare them the way a dictionary orders words: element by element from the front, so a
sequence that is a prefix of another comes first, and the empty sequence comes before
everything. In the worst case a comparison costs $O(|S|)$ work and $O(\log |S|)$ span,
but it stops as soon as it finds a difference.
par::array_sequence<int> A{1, 2, 3}, B{1, 2, 3}, C{1, 2, 4}, P{1, 2};
bool same = (A == B); // true
bool differ = (A != C); // true
bool smaller = (A < C); // true, they first differ at the last element, and 3 < 4
bool prefix = (P < A); // true, P is a prefix of A
take |
Work | Span |
|---|---|---|
|
$O(n)$ | $O(1)$ |
S.take(n) returns a new array_sequence holding the first
n elements of S. It is an error for n to exceed
the length of S.
drop |
Work | Span |
|---|---|---|
|
$O(|S| - n)$ | $O(1)$ |
S.drop(n) returns a new array_sequence holding everything
after the first n elements of S. It is an error for
n to exceed the length of S.
par::array_sequence<int> S{1,2,3,4};
auto a = S.take(2); // a == [1,2]
auto b = S.drop(2); // b == [3,4]
Both copy; they do not view the original. Changing S afterwards
leaves a and b untouched. This means they are expensive;
they cost linear work. This is unlike
subseq, which returns a view into
the original sequence in constant work. In most situations you should prefer subseq.
Since array_sequence mirrors std::vector, it also has
operations that change a sequence in place. push_back,
emplace_back, pop_back, clear,
resize, reserve, assign and swap
behave as their std::vector counterparts do. insert and
erase differ: they take an index rather than an iterator, so
S.insert(0, 9) puts 9 at the front and S.erase(0) removes the
first element.
They are also sequential, so growing a sequence an element at a time gives it span
proportional to its length, which throws away the parallelism the rest of this library
exists to provide. So, generally, you should not need any of them.
Prefer to build sequences with parallel operations such as tabulate,
map and filter.