4

I am writing a simulation engine consisting of a number of components, each of which operates on a fixed set of shared buffers.

In practice, the simulation will run entirely on the GPU. When developing a component however, it is easier to copy the buffers from the GPU, execute the component on the CPU, and write the updated buffers back, continuing on the GPU. When the component has been fully debugged, it is ported to a GPU kernel.

I want to clean up my code and write an interface to the 'main system' (the bit that maintains the buffers) that the components will use, and this raises the question of how to present the buffers.

I could write something like:

interface ISystem
{
    Array x;
    Array y;
    ComputeBuffer gpu_x;
    ComputeBuffer gpu_y;
    int numElements;
}

But this is not very neat.

I could do something like:

interface ISystem
{
    IBuffer x;
    IBuffer y;
    int numElements;
}

Where IBuffer is an interface suitable for use by code that wants CPU access to the buffer, but also that which binds the buffer to its GPU kernels.

My question is, how far should I push this abstraction?

I could make a truly polymorphic object like:

class BufferHelper<T>
{
    static implicit operator ComputeBuffer(BufferHelper helper);
    static implicit operator T[](BufferHelper helper);
}

Reading back to the CPU has significant performance implications. However, anyone who will write components will know this, and it will be obvious that this occurs, and where, from the profiler.

From a performance point of view then, there is no benefit to code self-documenting by having explicit interfaces for the two uses of the buffer, but still my instincts say that something is not right with this design.

In the traditional example of polymorphism you'd, say, add two integers or concatenate two strings; operations which are analogous if not identical.

In the above case, the operations (binding a buffer vs. reading it back and modifying it) are completely different.

To put it another way, does this use of polymorphism go so far as to decrease readability and maintainability of the code by hiding things the developer should see?

2 Answers 2

4

The way I picture your system, based on the given description, is as a set of worker components that read values from zero or more input buffers and write their results to zero or more output buffers. Besides these worker components, there is a management/configuration part that creates the components and buffers and connects them together in the correct way.

In such a system, the worker components should not care where the values in their input buffer(s) come from, nor where the values in the output buffer(s) go to. In particular, the worker components should not be aware if the buffer keeps the values on the same GPU, copies them to/from the CPU or even if the values are transferred between GPUs on different machines.

If you can design a buffer interface that can hide that kind of information from the worker components using it without incurring excessive overhead for the most common case (probably communication between components on the same GPU), then you should go for that.
Then it is the responsibility of the configuration component to select the buffer implementations that best meet the requirements for fast simulation and ease of debugging worker component X.

1
  • yes that picture is correct. I implemented the polymorphic object to see how it felt using it in practice. Though when using C# interfaces it wasn't as frictionless as in the question, it has made iteration quicker and does not introduce the ambiguities I feared.
    – sebf
    Commented Feb 20, 2018 at 20:49
1

If your goal is to separate the application call from the hardware you are OK. However, if your goal is to work with P/Invoke then you are introducing problems.

This is a case where you might wrap internal representation with an external interface. If you choose to do that, you are doing Composition which would be the way to present an easy to use interface to a hardware calls.

The issue with P/Invoke is that your struct that describes the data structure you send to the low level API has to match the bytes expected. That means you can't redefine your low level interfaces at will. You either have to choose to expose them to users or wrap them with your own API.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.