This repository has been archived on 2023-10-09. You can view files and clone it, but cannot push or open issues or pull requests.
Files
blender-archive/source/blender/blenlib/BLI_devirtualize_parameters.hh
Jacques Lucke 1bbf1ed03c Functions: improve devirtualization in multi-function builder
This refactors how devirtualization is done in general and how
multi-functions use it.

* The old `Devirtualizer` class has been removed in favor of a simpler
  solution. It is also more general in the sense that it is not coupled
  with `IndexMask` and `VArray`. Instead there is a function that has
  inputs which control how different types are devirtualized. The
  new implementation is currently less general with regard to the number
  of parameters it supports. This can be changed in the future, but
  does not seem necessary now and would make the code less obvious.
* Devirtualizers for different types are now defined in their respective
  headers.
* The multi-function builder works with the `GVArray` stored in `MFParams`
  directly now, instead of first converting it to a `VArray<T>`. This reduces
  some constant overhead, which makes the multi-function slightly
  faster. This is only noticable when very few elements are processed though.

No functional changes or performance regressions are expected.
2023-01-07 12:55:48 +01:00

165 lines
6.5 KiB
C++

/* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
/** \file
* \ingroup bli
*
* In geometry nodes, many functions accept fields as inputs. For the implementation that means
* that the inputs are virtual arrays. Usually those are backed by actual arrays or single values
* but sometimes virtual arrays are used to compute values on demand or convert between data
* formats.
*
* Using virtual arrays has the downside that individual elements are accessed through a virtual
* method call, which has some overhead compared to normal array access. Whether this overhead is
* negligible depends on the context. For very small functions (e.g. a single addition), the
* overhead can make the function many times slower. Furthermore, it prevents the compiler from
* doing some optimizations (e.g. loop unrolling and inserting SIMD instructions).
*
* The solution is to "devirtualize" the virtual arrays in cases when the overhead cannot be
* ignored. That means that the function is instantiated multiple times at compile time for the
* different cases. For example, there can be an optimized function that adds a span and a single
* value, and another function that adds a span and another span. At run-time there is a dynamic
* dispatch that executes the best function given the specific virtual arrays.
*
* The problem with this devirtualization is that it can result in exponentially increasing compile
* times and binary sizes, depending on the number of parameters that are devirtualized separately.
* So there is always a trade-off between run-time performance and compile-time/binary-size.
*
* This file provides a utility to devirtualize function parameters using a high level API. This
* makes it easy to experiment with different extremes of the mentioned trade-off and allows
* finding a good compromise for each function.
*/
namespace blender {
/**
* Calls the given function with devirtualized parameters if possible. Note that using many
* non-trivial devirtualizers results in exponential code growth.
*
* \return True if the function has been called.
*
* Every devirtualizer is expected to have a `devirtualize(auto fn) -> bool` method.
* This method is expected to do one of two things:
* - Call `fn` with the devirtualized argument and return what `fn` returns.
* - Don't call `fn` (because the devirtualization failed) and return false.
*
* Examples for devirtualizers: #BasicDevirtualizer, #IndexMaskDevirtualizer, #VArrayDevirtualizer.
*/
template<typename Fn, typename... Devirtualizers>
inline bool call_with_devirtualized_parameters(const std::tuple<Devirtualizers...> &devis,
const Fn &fn)
{
/* In theory the code below could be generalized to avoid code duplication. However, the maximum
* number of parameters is expected to be relatively low. Explicitely implementing the different
* cases makes it more obvious to see what is going on and also makes inlining everything easier
* for the compiler. */
constexpr size_t DeviNum = sizeof...(Devirtualizers);
if constexpr (DeviNum == 0) {
fn();
return true;
}
if constexpr (DeviNum == 1) {
return std::get<0>(devis).devirtualize([&](auto param0) {
fn(param0);
return true;
});
}
if constexpr (DeviNum == 2) {
return std::get<0>(devis).devirtualize([&](auto &&param0) {
return std::get<1>(devis).devirtualize([&](auto &&param1) {
fn(param0, param1);
return true;
});
});
}
if constexpr (DeviNum == 3) {
return std::get<0>(devis).devirtualize([&](auto &&param0) {
return std::get<1>(devis).devirtualize([&](auto &&param1) {
return std::get<2>(devis).devirtualize([&](auto &&param2) {
fn(param0, param1, param2);
return true;
});
});
});
}
if constexpr (DeviNum == 4) {
return std::get<0>(devis).devirtualize([&](auto &&param0) {
return std::get<1>(devis).devirtualize([&](auto &&param1) {
return std::get<2>(devis).devirtualize([&](auto &&param2) {
return std::get<3>(devis).devirtualize([&](auto &&param3) {
fn(param0, param1, param2, param3);
return true;
});
});
});
});
}
if constexpr (DeviNum == 5) {
return std::get<0>(devis).devirtualize([&](auto &&param0) {
return std::get<1>(devis).devirtualize([&](auto &&param1) {
return std::get<2>(devis).devirtualize([&](auto &&param2) {
return std::get<3>(devis).devirtualize([&](auto &&param3) {
return std::get<4>(devis).devirtualize([&](auto &&param4) {
fn(param0, param1, param2, param3, param4);
return true;
});
});
});
});
});
}
if constexpr (DeviNum == 6) {
return std::get<0>(devis).devirtualize([&](auto &&param0) {
return std::get<1>(devis).devirtualize([&](auto &&param1) {
return std::get<2>(devis).devirtualize([&](auto &&param2) {
return std::get<3>(devis).devirtualize([&](auto &&param3) {
return std::get<4>(devis).devirtualize([&](auto &&param4) {
return std::get<5>(devis).devirtualize([&](auto &&param5) {
fn(param0, param1, param2, param3, param4, param5);
return true;
});
});
});
});
});
});
}
if constexpr (DeviNum == 7) {
return std::get<0>(devis).devirtualize([&](auto &&param0) {
return std::get<1>(devis).devirtualize([&](auto &&param1) {
return std::get<2>(devis).devirtualize([&](auto &&param2) {
return std::get<3>(devis).devirtualize([&](auto &&param3) {
return std::get<4>(devis).devirtualize([&](auto &&param4) {
return std::get<5>(devis).devirtualize([&](auto &&param5) {
return std::get<6>(devis).devirtualize([&](auto &&param6) {
fn(param0, param1, param2, param3, param4, param5, param6);
return true;
});
});
});
});
});
});
});
}
return false;
}
/**
* A devirtualizer to be used with #call_with_devirtualized_parameters.
*
* This one is very simple, it does not perform any actual devirtualization. It can be used to pass
* parameters to the function that shouldn't be devirtualized.
*/
template<typename T> struct BasicDevirtualizer {
const T value;
template<typename Fn> bool devirtualize(const Fn &fn) const
{
return fn(this->value);
}
};
} // namespace blender