0

I am developing a key / value database

https://github.com/nmmmnu/HM4

Currently I have following commands

  • XNSUM - sum keys with same prefix
  • XRSUM - sum keys from some range
  • XNCOUNT - count keys with same prefix
  • XRCOUNT - count keys from some range

All this works well, except I have way too many commands - 10+

Now I have idea to add some more, so I want this to look like this:

  • XNPIPE prefix | SUM
  • XRPIPE start stop | SUM
  • XNPIPE prefix | COUNT
  • XRPIPE start stop | COUNT

For this I need to do some "API" / interface with virtual methods of function pointers.

Here is very basic idea.

Some reduce commands will require a copy of the input data to be made. (no real copy is done, but I store like 100'000 pointers to objects in array)
Then these will be done as user_map_copy which is very efficient with single vtable call.
Yes all data is copied but that's OK, because is required for other reasons.

However other operations, for example sum and count, does not need "copy" of the input and "copy" will be waste. But if we do not copy, we have to do 100'000 vtable call - user_map.

Because those 100'000 vtable call are to the same object, there should be something I can do, so these calls to be devirtualized. I can guarantee I wont change the vtable between the calls :)

user_map_test - here I am trying to devirtualize the call, without success.

Any idea how I can make user_map more optimal?

I am using C++17.


#include <algorithm>
#include <iterator>

struct BaseReducer{
    std::size_t result = 0;

    virtual void map(int a) = 0;
    virtual void map_range(const int *begin, const int *end) = 0;

    // no need the d-tor, but for completeness
    virtual ~BaseReducer() = default;
};

auto user_map(const int *begin, const int *end, BaseReducer &r){
    r.result = 0;

    for(auto it = begin; it != end; ++it){
        r.map(*it);
        // much more logic here
    }
    
    return r.result;
}

auto user_map_test(const int *begin, const int *end, BaseReducer &r){
    r.result = 0;

    void (BaseReducer::*f)(int) = &BaseReducer::map;

    for(auto it = begin; it != end; ++it)
        (r.*f)(*it);

    return r.result;
}

auto user_map_copy(const int *begin, const int *end, BaseReducer &r){
    // here we copy the elements and pass it to the BaseReducer
    // example is very basic so we do not copy, but real code does copy
    // and store pointers to real objects because they are very big.

    r.result = 0;

    int vector[5]; // let suppose know magically know the size

    std::copy(begin, end, vector);

    // ugly because we need pointers to iterate
    r.map_range(std::begin(vector), std::end(vector));

    return r.result;
}

struct SumReducer : BaseReducer{
    void map(int a) final{
            map_(a);
    }

    void map_range(const int *begin, const int *end) final{
        for(auto it = begin; it != end; ++it)
            map_(*it);
    }

private:
    void map_(int){
            ++result;
    }
};

struct CountReducer : BaseReducer{
    void map(int a) final{
            ++result;
    }

    void map_range(const int *begin, const int *end) final{
        result = static_cast<std::size_t>(end - begin);
    }
};

#include <iterator>

int main() {
    const int data[] = { 1, 2, 3, 4, 5 };

    CountReducer cr;

    user_map(std::begin(data), std::end(data), cr);

    std::cout << cr.result << '\n';

    user_map_copy(std::begin(data), std::end(data), cr);

    std::cout << cr.result << '\n';
}
10
  • 3
    Have you done any profiling/benchmarking/measurements?
    – JonasH
    Commented Oct 23 at 11:20
  • 1
    Repeating that. Have you measured the speed? 100,000 virtual calls to the same implementation don’t take more than a millisecond.
    – gnasher729
    Commented Oct 23 at 11:24
  • 1
    What makes you think you can do a better job in optimizing those 100000 calls than the optimizer that works much closer to the CPU? Hoisting invariant instructions out of a loop is a very common optimization. Commented Oct 23 at 12:12
  • 1
    @Nick: I see you are here, writing comments, still did not reply to the two very first questions asked - so let me repeat this: have you measured the speed? If the answer is "no", the most sensible answer is probably "you are overthinking this".
    – Doc Brown
    Commented Oct 23 at 12:24
  • 1
    Is it possible to implement everything via map_range and remove map from the public interface? If that's feasible, then the problem goes away. Commented Oct 26 at 6:51

2 Answers 2

0

I bet it doesn’t make a difference. If it does: Implement your function with a non virtual inline function named inline_f which has a T* pthis as the first argument and put it in the header file. Then call this instead of the virtual function, so inline_f (obj, args) instead of obj->f(args). To avoid code duplication implement f as inline_f(this, args).

This removes the virtual function call, and the non-virtual call, and allows optimisations if some of the arguments have known values.

4
  • inline_f(this, args) = can you give me an example, because I do not understand what exactly you mean?
    – Nick
    Commented Oct 23 at 11:41
  • see my answer to the question
    – Nick
    Commented Oct 23 at 14:03
  • When you say inline, you mean in reality a free aka non-member function, isn't it ? The question is then why the base class had an abstract member in the first place...
    – Christophe
    Commented Oct 23 at 18:56
  • As far as I know, the current optimizers are already able to remove virtual calls if the type can be known. And for sure, from performance point of view, the free function with the pointer argument and a non-virtual member function will make absolutely no difference and generate almost the same code.
    – Christophe
    Commented Oct 23 at 19:08
3

If you always have access to the fully-derived type you are using, you don't need virtual functions at all.

struct BaseReducer{
    std::size_t result = 0;
};

// CountReducer and SumReducer as before, but final qualifiers are not needed or allowed

template <typename Reducer>
auto user_map(const int *begin, const int *end, Reducer& r){
    r.result = 0;

    for(auto it = begin; it != end; ++it){
        r.map(*it);
        // much more logic here
    }
    
    return r.result;
}

template <typename Reducer>
auto user_map_copy(const int *begin, const int *end, Reducer& r){
    // here we copy the elements and pass it to the BaseReducer
    // example is very basic so we do not copy, but real code does copy
    // and store pointers to real objects because they are very big.
    r.result = 0;
   
    int vector[5]; // let suppose know magically know the size

    std::copy(begin, end, vector);

    // ugly because we need pointers to iterate
    r.map_range(std::begin(vector), std::end(vector));

    return r.result;
}

int main() {
    const int data[] = { 1, 2, 3, 4, 5 };

    CountReducer cr;

    user_map(std::begin(data), std::end(data), cr);

    std::cout << cr.result << '\n';

    user_map_copy(std::begin(data), std::end(data), cr);

    std::cout << cr.result << '\n';
}
12
  • templates - yes. this is kind of how it works at the moment, but this wont work if the end user have to choose the type of the reduce command.
    – Nick
    Commented Oct 23 at 12:16
  • 2
    @Nick yes it can. switch (userInput) case Count: do_pipeline<CountReducer>(); break; case Sum: do_pipeline<SumReducer>(); break; etc
    – Caleth
    Commented Oct 23 at 12:20
  • @Nick or probably more scalably: std::variant<Reducer1, Reducer2, ...> input_one = gather_input_one(); std::variant<ReducerA, ReducerB, ...> input_two = gather_input_two(); ... std::visit(do_pipeline, input_one, input_two, ...);
    – Caleth
    Commented Oct 23 at 12:23
  • yes, I am thinking about this way too. but then the modules will be more coupled because they will be part of the type, this is something I want to avoid
    – Nick
    Commented Oct 23 at 12:24
  • They are only more coupled if you are separately compiling your project. user_map doesn't depend on the concrete reducer types, it still only depends on the abstraction. The code calling user_map, which is already coupled to the concrete types, remains coupled to the concrete types.
    – Caleth
    Commented Oct 23 at 12:29

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.