Processing large amounts of data (gigabytes) I use indexes to data arrays. Since access to data could lead to cache inefficiency, I want to cache some data from array together with the index which gives dramatic speedup for operations through indexes.
The amount of cached data is compile-time choice which should include zero amount of cache data. I have large amount of indexes, so in this case I don’t want to pay for extra “empty” element like std::array
does, for example.
So, I made a template with a specialization:
using index_t = unsigned int;
using lexem_t = unsigned int;
template <std::size_t t_arg_cache_line_size>
struct lexem_index_with_cache_t {
index_t index;
std::array<lexem_t, t_arg_cache_line_size> cache_line;
constexpr std::size_t cache_line_size() const {
return t_arg_cache_line_size;
}
};
template<>
struct lexem_index_with_cache_t<0> {
index_t index;
static std::array<lexem_t, 0> cache_line;
constexpr std::size_t cache_line_size() const {
return 0;
}
};
std::array<lexem_t, 0> lexem_index_with_cache_t<0>::cache_line;
The problem is this “hack” I used in the specialization with zero size which utilizes static member to give formal access to the cache_line
while is it empty and the access is not really needed. This allows me to avoid specializations in functions which use this template, like here:
using lexem_index_with_cache = lexem_index_with_cache_t<0>;
template <typename T>
class seq_forward_comparator_cached
{
const std::vector<T>& vec;
public:
seq_forward_comparator_cached(const std::vector<T>& vec) : vec(vec) { }
bool operator() (const lexem_index_with_cache& idx1, const lexem_index_with_cache& idx2)
{
if (idx1.index == idx2.index) {
return false;
}
const auto it1_cache_line = idx1.cache_line; // This code wouldn’t compile in absence of static “hack”
const auto it2_cache_line = idx2.cache_line; // This code wouldn’t compile in absence of static “hack”
auto res = std::lexicographical_compare_three_way(
it1_cache_line.begin(), it1_cache_line.end(),
it2_cache_line.begin(), it2_cache_line.end());
if (res == std::strong_ordering::equal) {
auto range1 = std::ranges::subrange(vec.begin() + idx1.index + idx1.cache_line_size(), vec.end());
auto range2 = std::ranges::subrange(vec.begin() + idx2.index + idx2.cache_line_size(), vec.end());
return std::ranges::lexicographical_compare(range1, range2);
}
return res == std::strong_ordering::less;
}
};
Of course, I can implement another template specialization of this template for zero size cache, but this will lead to code duplication and I have many such functions, so I don’t want to specialize all of them.
What is a proper way in modern C++ to avoid this static
hack and possible code duplication on the other hand?
I am not sure, maybe some kind of conditional code include depending on the type could help.
I would like to avoid wrapping access to cache_line
to a function, but if this is the only case, please give a clue on the approach.
The compilable code is here.
I've added an example with a trick to use the if constexpr(...)
in non-template code.
It is now impossible to actually use the at()
function accidentally, as opposed to the static member solution.
#include <array>
using data_type = int;
template<size_t _data_size>
class ExtendableIndex
{
public:
constexpr static size_t data_size = _data_size;
data_type& at(size_t idx) { return data[idx]; }
size_t index;
std::array<data_type, _data_size> data;
};
template<>
class ExtendableIndex<0>
{
public:
constexpr static size_t data_size = 0;
data_type& at(size_t idx);
size_t index;
};
using DefaultIndex = ExtendableIndex<0>;
class DataUser
{
public:
void process(DefaultIndex& index)
{
if constexpr (DefaultIndex::data_size > 0)
{
// auto value = index.data[0]; // -> this fails to compile
auto value = index.at(0); // -> but this slight workaround solves the issue, `at()` is not implemented and thats OK.
}
}
template<size_t _data_size>
void process_template(ExtendableIndex<_data_size>& index)
{
if constexpr (DefaultIndex::data_size > 0)
{
auto value = index.data[0]; // -> this compiles even if index.data doesn't exist when 'process' is a template
}
}
};
int main()
{
DataUser r;
ExtendableIndex<0> index_zero;
r.process(index_zero);
r.process_template(index_zero);
ExtendableIndex<1> index_one;
r.process_template(index_one);
}