• No results found

Algorithm Selection

Chapter 8. Coordinates with Mutable Successors

4. During the call of TRaverse_rotating (c, proc), the total number of calls of

10.5. Algorithm Selection

In Section 10.3 we presented reverse algorithms with a variety of iterator requirements and procedure signatures, including versions taking counted and bounded ranges. It is worth defining variations that make the most convenient signatures available for additional iterator types. For example, an additional constant-time iterator difference leads to the algorithm for reversing a bounded range of indexed iterators:

template<typename I>

requires(Mutable(I) && IndexedIterator(I)) void reverse_indexed(I f, I l)

{

// Precondition: mutable_bounded_range(f, l) reverse_n_indexed(f, l - f);

}

When a range of forward iterators must be reversed, there is usually enough extra memory available to allow reverse_n_adaptive to run efficiently. When the size of the range to be reversed is

moderate, it can be obtained in the usual way (for example, malloc). However, when the size is very large, there might not be enough available physical memory to allocate a buffer of this size. Because algorithms such as reverse_n_adaptive run efficiently even when the size of the buffer is small in proportion to the range being mutated, it is useful for the system to provide a way to allocate a temporary buffer. The allocation may reserve less memory than requested; in a system with virtual memory, the allocated memory has physical memory assigned to it. A temporary buffer is intended for short-term use and is guaranteed to be returned when the algorithm terminates.

For example, the following algorithm uses a type temporary_buffer:

template<typename I>

requires(Mutable(I) && ForwardIterator(I))

void reverse_n_with_temporary_buffer(I f, DistanceType(I) n) {

// Precondition: mutable_counted_range(f, n) temporary_buffer<ValueType(I)> b(n);

reverse_n_adaptive(f, n, begin(b), size(b)); }

The constructor b(n) allocates memory to hold some number m n adjacent objects of type ValueType(I); size(b) returns the number m, and begin(b) returns an iterator pointing to the beginning of this range. The destructor for b deallocates the memory.

For the same problem, there are often different algorithms for different type requirements. For example, for rotate there are three useful algorithms for indexed (and random access), bidirectional, and forward iterators. It is possible to automatically select from a family of algorithms, based on the requirements the types satisfy. We accomplish this by using a mechanism known as concept dispatch. We start by defining a top-level dispatch procedure, which in this case also handles trivial rotates:

template<typename I>

requires(Mutable(I) && ForwardIterator(I)) I rotate(I f, I m, I l)

{

// Precondition: mutable_bounded_range(f, l) m ϵ [f, l] if (m == f) return l;

if (m == l) return f;

return rotate_nontrivial(f, m, l, IteratorConcept(I)()); }

concept modeled by its argument. We then implement a procedure for each concept tag type:

template<typename I>

requires(Mutable(I) && ForwardIterator(I))

I rotate_nontrivial(I f, I m, I l, forward_iterator_tag) { // Precondition: mutable_bounded_range(f, l) f m l return rotate_forward_nontrivial(f, m, l); } template<typename I>

requires(Mutable(I) && BidirectionalIterator(I))

I rotate_nontrivial(I f, I m, I l, bidirectional_iterator_tag) { // Precondition: mutable_bounded_range(f, l) f m l return rotate_bidirectional_nontrivial(f, m, l); } template<typename I>

requires(Mutable(I) && IndexedIterator(I))

I rotate_nontrivial(I f, I m, I l, indexed_iterator_tag) { // Precondition: mutable_bounded_range(f, l) f m l return rotate_indexed_nontrivial(f, m, l); } template<typename I>

requires(Mutable(I) && RandomAccessIterator(I))

I rotate_nontrivial(I f, I m, I l, random_access_iterator_tag) {

// Precondition: mutable_bounded_range(f, l) f m l

return rotate_random_access_nontrivial(f, m, l); }

Concept dispatch does not take into consideration factors other than type requirements. For example, as summarized in Table 10.1, we can rotate a range of random-access iterators by using three algorithms, each performing a different number of assignments. When the range fits into cache memory, the n + gcd(n, k) assignments performed by the random-access algorithm give us the best performance. But when the range does not fit into cache, the 3n assignments of the bidirectional algorithm or the 3(n–gcd(n, k)) assignments of the forward algorithm are faster. In this case additional factors are affecting whether the bidirectional or forward algorithm will be fastest, including the more regular loop structure of the bidirectional algorithm, which can make up for the additional

assignments it performs, and details of the processor architecture, such as its cache configuration and prefetch logic. It should also be noted that the algorithms perform iterator operations in addition to assignments of the value type; as the size of the value type gets smaller, the relative cost of these other operations increases.

Table 10.1. Number of Assignments Performed by Rotate Algorithms Algorithm Assignments indexed, random_access n + gcd (n, k) bidirectional 3n or 3(n – 2) forward 3(n – gcd (n, k)) with_buffer n +(n – k) with_buffer_backward n + k partial 3k

Project 10.1.

Project 10.2.

Note: where n = l – f and k = l – m

Design a benchmark comparing performance of all the algorithms for different array sizes, element sizes, and rotation amounts. Based on the results of the benchmark, design a composite algorithm that appropriately uses one of the rotate algorithms depending on the iterator concept, size of the range, amount of rotation, element size, cache size, availability of temporary buffer, and other relevant considerations.

We have presented two kinds of position-based rearrangement algorithms: reverse and rotate. There are, however, other examples of such algorithms in the literature. Develop a taxonomy of position-based rearrangements, catalog existing algorithms, discover missing algorithms, and produce a library.

Algorithms C++ Software Engineering Programming Alexander Stepanov Paul McJones Addison-Wesley Professional Elements of Programming

10.6. Conclusions

The structure of permutations allows us to design and analyze rearrangement algorithms. Even simple problems, such as reverse and rotate, lead to a variety of useful algorithms. Selecting the appropriate one depends on iterator requirements and system issues. Memory-adaptive algorithms provide a practical alternative to the theoretical notion of in-place algorithms.

Algorithms C++ Software Engineering Programming Alexander Stepanov Paul McJones Addison-Wesley Professional Elements of Programming