DepSpawn 1.2
 
Loading...
Searching...
No Matches
Tips & tricks

Freezing arguments

Sometimes one may need to freeze an argument to a task, so that it is not affected by future changes of the value of the object. A typical example of this situation would be the code

for(i = 0; i < N; i++)
spawn(f, w, i);

in which we want each task to work with a different value of i. Unfortunately, i is being modified in each iteration of the loop, so when the task generated during a given iteration x begins its execution, it will probably find that i has no longer the value x.

Another situation in which we would need to freeze an argument is when it may have already destroyed when the task begins its execution. Let us have a look for example to this code:

void my_function(Widget &w) {
int i = w.something();
spawn(f, w, i);
}
...
my_function(my_widget);

Do you see the problem? f will try to access i as one of its arguments when it begins its execution, but at that point very probably the thread that spawed the task will have already left function my_function, thus destroying i.

Fortunately, freezing an argument to a spawned task is very easy: it suffices to turn it into an rvalue by means of the std::move function found in the <utility> standard header. Thus, the correct coding for our examples would be spawn(f, w, std::move(i)) in both of them.

Notice that freezing an argument has a secondary effect: since the task works with a frozen copy of the variable, it does not have to track dependencies on that argument and it does not generate new dependencies for subsequent tasks that use the frozen variable either. Also, since the task receives a copy, any change will be made on the copy, so even if the corresponding argument is a non-const reference, those changes will not be reflected in the original variable.

Using pointers to avoid generating false dependencies

The requirement that a task must fulfill all the dependencies on its arguments before it can begin its execution always leads to correct executions, but sometimes it can limit the performance unnecessarily. Imagine we have this piece of code

void does_more_stuff(Widget &w) {
.... // Part 1: does not use w
spawn(works_on_widget, w, other_stuff);
... // Part 2: can be done in parallel with works_on_widget
... // Part 3: do something with w
}
spawn(f, ..., my_widget, ...);
spawn(does_more_stuff, my_widget);
void wait_for(const Args &... args)
Wait for a specific group of variables to be written.
Definition: depspawn.h:665

where works_on_widget modifies its first argument. Since does_more_stuff receives its argument by means of a non-const reference, it will only begin its execution once f finishes, as there is a dependency on my_widget. While this is correct, we can see that does_more_stuff does not actually interact with w until Part 3 . Thus we could actually parallelize the execution of f with Parts 1 and 2 of does_more_stuff if we could pass to does_more_stuff its argument in such a way that this dependency were not seen. This could be achieved using what we learned on pointers in Express dependencies by means of the parameter types, which is basically that a pointer carries dependences on the pointer itself, but not on the object(s) it points to. As a result, if we wrote our code like this:

void does_more_stuff(Widget *wp) {
Widget& w = *wp;
.... // Part 1: does not use w
spawn(works_on_widget, w, other_stuff);
... // Part 2: can be done in parallel with works_on_widget
... // Part 3: do something with w
}
spawn(f, ..., my_widget, ...);
spawn(does_more_stuff, &my_widget);

DepSpawn would not see any dependency between f and does_more_stuff and it would run them in parallel. The dependencies on works_on_widget and the wait_for statement would be honored, as they are expressed on the object itself, not on a pointer to it.

A general mechanism to avoid generating dependencies

The pointer pointer trick requires changing both the API and the invocation of a function, which may be sometimes undesirable. Also, it changes permanently the semantics of the associated function parameter, while it may be the case that we only want to avoid dependencies in some specific invocation(s) of a function. For this reason, DepSpawn provides two other mechanisms to avoid generating dependencies:

  • ignore() is a function such that if an argument x to a spawn invocation is written like ignore(x), any dependency on x will be ignored for the sake of this task. This means that, just as with the pointer trick, it will neither wait for pending writes or reads to x nor generate new dependencies for future tasks that could access x.
  • If a format parameter of a function f that used to have type T is changed to have type Ignore<T>, similarly to what happened with the pointer trick, it will never track or generate dependencies on the argument provided for that parameter. The advantage here is that, contrary to the pointer trick, we can continue to send the argument "as is" to f instead of its address. Ignore<T> provides an implicit conversion operator to the type T to access its content.

Overloaded functions

The compiler can get confused when there are several versions of the function to spawn. The current approach to solve this is to provide the function type to spawn by means of a template argument. Here is an example:

void f(int& i) { ... }
void f(float& f) { ... }
...
spawn<void(&)(int&)>(f, an_int);
spawn<void(&)(float&)>(f, a_float);

Sequential execution

If the macro SEQUENTIAL_DEPSPAWN is defined during the compilation of a source file, its spawn() operations will be replaced by sequential function invocations.