Intel® C++ Compiler 16.0 User and Reference Guide

Advanced Topic: How to Write a New Reducer

You can write a custom reducer if none of the supplied reducers satisfies your requirements.

Any of the supplied reducers can be used as models for developing new reducers, although some of these examples are relatively complex. The implementations are found in the reducer_*.h header files in the include/cilk directory of the installation.

Additional examples are provided in the following sections.

The basic reducer infrastructure is in include/cilk/reducer.h.

Monoids

In mathematics, a monoid comprises a set of values T, an associative operation OP on those values, and an identity I value with respect to the operation. That is, (T, OP, I) is a monoid if:

Examples of monoids include the following:

A reducer is always built around a monoid. That is, it computes a final value by combining a set of values with an operator; it relies on the associativity of the operator to be able to recombine parts of the combination in parallel; and it relies on having an identity value to be able to initialize the accumulator variables for the partial computations.

Components of a Reducer

A reducer can be broken into these logical parts:

The Monoid Class

A monoid class provides the information needed by the reducer class to create, initialize, merge, and destroy view instances. It must define types value_type, the value type of the reducer, and view_type, the view class for the reducer, and the following functions which may be either static or const:

Monoid Function

Description

reduce(value_type *left, value_type *right)

evaluates *left = *left OP *right; leaves *right in an unspecified, but valid state

identity(value_type *p)

constructs the identity value into the uninitialized memory pointed to byp

destroy(value_type *p)

calls the value_type destructor on the object pointed to by p

allocate(size)

returns a pointer to size bytes of raw memory

deallocate(p)

deallocates the raw memory at p

Any class that provides these definitions can be used as a monoid class, but in practice, a monoid class is always defined as a subclass of cilk::monoid_base<Value, View=Value>, which defines view and value types to be View and Value and defines the allocate(), destroy(), and deallocate() functions using the new operator. A class derived from monoid_base needs to declare and implement only the identity() and reduce() functions.

The reduce() function is not required to leave any particular value in its right argument view, but it must leave it in a valid state so that it can be destructed following the reduce().

Why Associativity Is Necessary

For a reducer to deterministically reproduce the serial semantics, the reduce function and the permitted operations on the view must implement an associative operation. (It does not need to be commutative.)

The serial execution of reduction computes the expression (a0 OP a1 OP a2 OP … OP aN ). When the reduction is performed in parallel, that expression is broken up into a set of subexpressions which are then combined; for example, ((a0 OP a1 OP a2 ) OP ((a3 OP a4 ) OP (a5 ))) OP (a6 OP a7 OP … OP a10 ) OP …. The partitioning into subexpressions, and the order in which the subexpressions are combined, is unpredictable, and can vary from one run of the program to another. However, If OP is associative, it does not matter: regardless of how the expression is broken up, and in what order the subexpressions are associated, the result will be the same.



Writing Reducers - A Singly-Linked List

Here is a simple singly-linked list created in a loop. The example shows code to make the loop parallel by using a cilk_for, but it will not work, because there will be data races between the list node updates. To create a value incrementally in parallel, use of a reducer is required.

#include <cilk/cilk.h>
#include <iostream>

//  Singly-linked list.
//
struct IntListNode {
    int data;
    IntListNode* link;
};

// Compute a value. (Probably does something more interesting in 
// a real program.)
//
int compute(int i)
{
    return i;
}

// Create a list.
//
IntListNode* make_list(int n)
{
    IntListNode* list = 0;
    for (int i = 0; i < n; ++i) {
        IntListNode* node = new IntListNode;
        node->data = compute(i);
        node->link = list;
        list = node;
    }
    return list;
}

// Use a list. (Probably does something more interesting in a 
// real program.)
//
void print_list(IntListNode* list)
{
    for (IntListNode* node = list; node; node = node->link)
        std::cout << node->data << "\n";
}

int main()
{
    IntListNode* list = make_list(20);
    print_list(list);
   // Code to deallocate list should go here
}

The first step in creating a reducer is to identify the monoid. The data type here is a singly-linked list of integers, represented by IntListNode objects. Since parallelizing the computation will result in creating and combining multiple sublists, the associative operation must be list concatenation. The identity value for list concatenation is the empty list. Adding an object at the beginning of a list is equivalent to concatenating a new singleton list containing the object to be added at the front of the list ( list = {x} || list ), so the reduce operation must be " left = right || left ".

How is the list to be represented? Is the view type different from the value type? A singly-linked list can be uniquely represented by a pointer to its first node (or a null pointer for an empty list), and this is what is returned from the make_list function and passed to the print_list function. However, there is no efficient way to concatenate two lists represented just by pointers to their heads, so the view type needs both a head and a tail pointer.

So this is the structure of a monoid. The value type is a pointer to the first node in the list, or null for an empty list. The view is a (head, tail) pointer pair, and provides an operation to add a new value to the list. The monoid's identity function initializes an empty-list view, and its reduce() function concatenates the lists represented by two views.

Here is the resulting parallel code using the new reducer:

#include <cilk/cilk.h>
#include <cilk/reducer.h>
#include <iostream>

//  Singly-linked list.
//
struct IntListNode {
    int data;
    IntListNode* link;
};


class IntListMonoid;            // forward declaration

//  View class.
//
class IntListView {
    friend class IntListMonoid; // for the identity and reduce functions
    IntListNode* head;
    IntListNode* tail;
public:
    void add_value(int x) { 
        IntListNode* node = new IntListNode;
        node->data = x;
        node->link = head;
        head = node;
        if (!tail) tail = node;
    }
    IntListNode* get_value() const { return head; }
};

// Monoid class.
//
struct IntListMonoid : public cilk::monoid_base<IntListNode*, IntListView> {

    // Set *view to the empty list.
    //
    static void identity(IntListView* view) {
        view->head = view->tail = 0;
    }
    
    // Move the right list to the beginning of the left list.
    // Leave the right list empty.
    //
    static void reduce(IntListView* left, IntListView* right) {
        if (right->head) {
            right->tail->link = left->head;
            left->head = right->head;
            right->head = right->tail = 0;
        }
    }
};


// Compute a value. (Probably does something more interesting in 
// a real program.)
//
int compute(int i)
{
    return i;
}

// Create a list.
//
IntListNode* make_list(int n)
{
    cilk::reducer<IntListMonoid> list;
    cilk_for (int i = 0; i < n; ++i) {
        list->add_value(i);     // "list->" accesses the view
    }
    return list.get_value();   // get_value() is a reducer function
}

// Use a list. (Probably does something more interesting in a 
// real program.)
//
void print_list(IntListNode* list)
{
    for (IntListNode* node = list; node; node = node->link)
        std::cout << node->data << "\n";
}

int main()
{
    IntListNode* list = make_list(20);
    print_list(list);
   // Code to deallocate list should go here
}

For reducers with simple views like this one, the coding can be simplified further. In the view class:

Then use the monoid_with_view class instead of monoid_base, and you do not need to specialize the monoid class at all:

//  View class.
//
class IntListView {
    IntListNode* head;
    IntListNode* tail;

public:
    typedef IntListNode* value_type;
    IntListView() : head(0), tail(0) {}
    void reduce(IntListView* right) { 
        if (right->head) {
            right->tail->link = this->head;
            this->head = right->head;
            right->head = right->tail = 0;
        }
    }
    void add_value(int x) { 
        IntListNode* node = new IntListNode;
        node->data = x;
        node->link = head;
        head = node;
        if (!tail) tail = node;
    }
    IntListNode* get_value() const { return head; }
};

// Monoid class.
//
typedef cilk::monoid_with_view<IntListView> IntListMonoid;