computablee / DotMP

A collection of powerful abstractions for parallel programming in .NET with an OpenMP-like API.
https://computablee.github.io/DotMP/
GNU Lesser General Public License v2.1
29 stars 8 forks source link

Add `private`, `firstprivate`, and `shared` to `ParallelRegion` and `ParallelFor` #9

Closed computablee closed 1 year ago

computablee commented 1 year ago

This is a bit of a tricky one, and I'm not sure the best way to do this. Was thinking we could brainstorm in this issue before I assign anyone.

OpenMP supports private, firstprivate, and shared to parallel constructs as follows:

int a, b, c, d;
a = b = c = d = 0;

#pragma omp parallel private(a) firstprivate(b) shared(c,d)
{
    // Each thread now has a local copy of `a', though it is uninitialized
    // Each thread now has a local copy of `b', and it is initialized to 0
    // Each thread shares `c' and `d'
}

I had an idea of implementing this in OpenMP.NET as follows:

int a, b, c, d;
a = b = c = d = 0;

OpenMP.Parallel.ParallelRegion(priv: new object[] { a },
                               firstpriv: new object[] { b },
                               shared: new object[] { ref c, ref d },
                               action: (ref a, ref b, ref c) => {
    // Each thread now has a local copy of `a', though it is uninitialized
    // Each thread now has a local copy of `b', and it is initialized to 0
    // Each thread shares `c' and `d'
});

There are lots of problems with this idea. Let's unpack.

First, you can't throw ref parameters into an array, at least not from any of my searches on Google. So the shared parameter won't work. Second, we would have to trust the user that the action parameters are in-order for how they're presented to the function. Additionally, if the user swaps the placement of priv and firstpriv in the ParallelRegion call, we have no way of knowing this, and arguments would be passed to the action out-of-order. It would be a nightmare from the usability perspective.

Another glaringly obvious problem with this is that ParallelRegion accepts Action action as the lambda. If we were to start passing in reference parameters, we'd need to not only create some sort of ActionRef delegate as described here, but it would add horrible complexity to the code. Consider the following example of a simple ParallelRegion implementation with these new changes. For the sake of demonstrating this example in isolation, we pretend that you can only pass one variable to shared.

delegate void ActionRef<T>(ref T a);

public void ParallelRegion<T>(ref object shared, ActionRef<T> action)
{
    action(/* huh??? */);
}

Under this idea, we would...

  1. Need to create enough ActionRef overloads as as many variables we're willing to support (horrible complexity).
  2. Unbox/downcast shared and pass it as a reference parameter (not possible under C#'s type system).

The issues with shared can be mitigated if we just never implement it, and explain to the user that OpenMP.NET works as shared-by-default (via closures). If we can't find a way to implement shared, this would be a fine solution. However, the problems with ActionRef complexity and swapping priv and firstpriv remain issues.

So it looks like implementing these clauses is going to be a very non-trivial endeavor. Let's brainstorm!

computablee commented 1 year ago

I think this issue is now a non-issue. shared can be implemented merely by declaring a variable outside of the ParallelRegion or through the OpenMP.Shared<T> class, and private/firstprivate can be implemented by declaring a variable inside of the ParallelRegion. Marking this as "wontfix" and closing it for now. May re-open it later down the road.