4

I have two performance-critical functions like this:

insertExpensive(Holder* holder, Element* element, int index){
    //............ do some complex thing 1 
    holder->ensureRange(index);//a little expensive
    //............ do some complex thing 2
}
insertCheap(Holder* holder, Element* element, int index){
    //............ do some complex thing 1
    //............ do some complex thing 2
}

How to group 2 functions together to increase maintainability?

My poor solutions:

Solution 1.

insertExpensive(Holder* holder, Element* element, int index){
    do1();
    holder->ensureRange(index);//a little expensive
    do2();
}
insertCheap(Holder* holder, Element* element, int index){
    do1();
    do2();
}

It would be ugly. It also impractical if do2 want some local variables from do1.

Solution 2.

insert(Holder* holder, Element* element, int index, bool check){
    //............ do some complex thing 1 
    if(check)holder->ensureRange(index);//a little expensive
    //............ do some complex thing 2
}

It costs a conditional checking for every call.

Solution 3. (draft)

template<bool check> insert(Holder* holder, Element* element, int index){
    //............ do some complex thing 1       (Edit2 from do1());
    bar<check>();
    //............ do some complex thing 2       (Edit2 from do2());
}
template <>
inline void base_template<true>::bar() {  holder->ensureRange(index); }
template <>
inline void base_template<false>::bar() {  }

Overkill and unnecessary complexity?

Edit 1: The priority of criteria for how good an approach is, are sorted as followed:-
1. Best performance
2. Less duplicate of code
3. Less total line of code
4. Easier to read for expert & beginner

Edit 2: edit the 3rd solution. Thank mvidelgauz and Wolf.

Wolf
  • 9,679
  • 7
  • 62
  • 108
javaLover
  • 6,347
  • 2
  • 22
  • 67
  • 2
    How do you define a good solution? – Humam Helfawi Aug 07 '16 at 08:29
  • @Humam Helfawi Good point! The priority is sorted as followed:- 1. Best performance 2. Less duplicate of code 3. Less total line of code 4. Easier to read for expert & beginner ..... I will edit the question. – javaLover Aug 07 '16 at 08:33
  • 1
    you could create two classes that hold Expensive and Cheap objects, and when you need to add a cheap object you call the Insert method on a cheap object, and when you want to insert an expensive object, you call the same method on the Expensive object. – meJustAndrew Aug 07 '16 at 08:35
  • 1
    Thank. I forgot to think in that aspect, but how those methods implemented? IMHO, inside both methods would suffer the same symptom : duplication of code, and now it is also even harder to track because they appear in 2 different classes. – javaLover Aug 07 '16 at 08:38
  • 1
    I find the API important, that's why I'd resist to add an additional parameter, but of course the code duplication is bad. Look at my answer for a possible solution. – Wolf Aug 07 '16 at 09:01
  • 1
    This would better suit [Programmers SE site](http://programmers.stackexchange.com/) – Zereges Aug 07 '16 at 09:02
  • The third option is the best candidate for your performance parameter. Both implementations of `bar` will most likely be inlined (even without the the `inline` specification [it's kinda redundant for templates anyway]) – StoryTeller - Unslander Monica Aug 07 '16 at 09:04
  • Sorry, one more counterquestion: are this functions or (static) methods of a class? – Wolf Aug 07 '16 at 09:10
  • @Wolf I have both cases of the problem - static function and non static function, but for this question, it is non static. – javaLover Aug 07 '16 at 09:15

5 Answers5

6

Your solution 2 is actually not yet that bad. If this code is within a header, it is implicitly considered as inline code. (I've written it explicitly) If you call it with either true or false, the compiler can remove the if-statement, though it depends on a range of factors to know if it will do so. (Size of the whole body after inlining, visibility of the constant, tuning ...)

inline void insert(Holder* holder,Element* element,int index, bool check){
    do1();
    if (check)
        holder->ensureRange(index);//a little expensive
    do2();
}

The solution 3 is actually what you want to achieve, as templates require a new function instantiation for each different call, so it would remove the dead code. However it can be written very similar to how you wrote solution 2.

template <bool check>
inline void insert(Holder* holder,Element* element,int index){
    do1();
    if (check)
        holder->ensureRange(index);//a little expensive
    do2();
}

If you have C++17, you no longer have to depend on the compiler to remove dead code as you can enforce it to skip certain code via constexpr-if. This construction will guarantee that the code in the if-statement is removed as it ain't even have to compile.

template <bool check>
inline void insert(Holder* holder,Element* element,int index){
    do1();
    if constexpr (check)
        holder->ensureRange(index);//a little expensive
    do2();
}
JVApen
  • 11,008
  • 5
  • 31
  • 67
  • Pretty good answer, although I think you've mixed up what makes a function implicitly inline : being inside a class definition does, but simply being in a header doesn't (you'd get multiple definition errors at link-time). – Quentin Aug 07 '16 at 09:36
  • FYI: http://stackoverflow.com/questions/9192077/is-inline-implicit-in-c-member-functions-defined-in-class-definition By default, if the function is implemented in the header it gets treated as inline. (or as much as the inline keyword is worth) for templated functions this ain't the case as they always have to be in the header. Though in case of the templated examples here, it might have been better to remove the inline from them. – JVApen Aug 07 '16 at 11:32
  • 1
    That question is about member functions (which are implicitly `inline` if defined inside the class body). Try defining a free-standing function such as `void foo() {}` inside a header, including it into two cpp files and linking the resulting objects together : you'll probably get a "multiple definitions" error ("probably", because the Standard does not require a diagnostic IIRC) : the function has not been implicitly made `inline`. – Quentin Aug 07 '16 at 11:40
  • 1
    Agree, that is my bad. I'm barely writing free function lately – JVApen Aug 07 '16 at 12:01
2
insert(Holder* holder,Element* element,int index, bool ensureRange){
    //............ do some complex thing 1 
    if (ensureRange){
        holder->ensureRange(index);//a little expensive
    }
    //............ do some complex thing 2
}

And if you can make decision at compile time and want to employ templates:

template<bool check> insert(Holder* holder,Element* element,int index){
    //............ do some complex thing 1;
    if(check)holder->ensureRange(index);
    //............ do some complex thing 2
}


insert<true>(...); //expensive operation
insert<false>(...); //cheap operation
mvidelgauz
  • 2,176
  • 1
  • 16
  • 23
  • Thank, but you just copy my 2nd solution. :( – javaLover Aug 07 '16 at 08:31
  • 1
    you are correct, my bad i didn't go over all your code line-by-line. Still, this is my answer for your question _"How to group 2 functions together to increase maintainability?"_, I could just answer _"your 2nd option"_ – mvidelgauz Aug 07 '16 at 08:34
  • @javaLover I edited my answer for the case when you want to make decision at compile time using template – mvidelgauz Aug 07 '16 at 08:43
  • 1
    @javaLover no, I think, it's not just a copy, and that's your fault: you copied the *doX* functions in your second example. – Wolf Aug 07 '16 at 08:43
  • 3
    "It costs a conditional checking for every call". So? Is there a **measurable** impact on performance? – n. m. could be an AI Aug 07 '16 at 08:55
  • 2
    Good point as always, n.m. I have measured a similar case, and after solve, it save 5-10% of CPU. I have never measured this specific case though, and I accept that I am not so sure if there is any impact. But for this scope of question, let's assume that there is. – javaLover Aug 07 '16 at 08:59
  • @n.m. depends on circumstances. I know we are are not at the beginning of computer era and readability is favored over performance and so on... But believe or not even today I have a project where such thing matters (1000 DMA interrupts per second) – mvidelgauz Aug 07 '16 at 09:01
  • I think this is the most **straight forward** solution to this kind of problem that one should definitely have at hand. But it is less readable on caller side, and it degrades with the amount of parameters already present, i.e. it "scales badly" ;) – Wolf Aug 07 '16 at 09:40
1

Let's assume you make the following structure:

A class which define two protected methods do1 and do2 and a public abstract method Insert.

class BaseHolder
{
proteted:

void do1(/* complete with necessary parameters*/)
{

}

void do2(/* complete with necessary parameters*/)
{

}

public:
abstract void Insert(Holder* holder, Element* element, int index);

};

class Expensive : BaseHolder
{
public:
void Insert(Holder* holder, Element* element, int index)
{
    do1();
    holder->ensureRange(index);
    do2();
}
};

class Cheap : BaseHolder
{
public:
void Insert(Holder* holder, Element* element, int index)
{
    do1();
    do2();
}
};

Sorry if I have made some syntax mistakes, but this is the way I see a solution.


Another possiblity is to make a custom Cheap and Expensive classes which both wrapp a Holder, and in an Expensive constructor do the validate for range:

class Base
{
protected:
Holder* _holder;
public:
Holder* GetHolder(){ return _holder; }
}

class Cheap : Base
{
    public:
    Cheap(Holder* holder)
    {
        _holder = holder;
    }
};

class Expensive : Base
{
    public:
    Expensive(Holder* holder)
    {
         holder->ensureRange(index);
         _holder = holder;
    }
};

An use Cheap and Expensive objects as parameters to Insert method. I guess the second solution is better than the first one.


And a solution more like the first one, but which uses the template method design pattern, as the best comes at the end:

class BaseHolder
{
proteted:

void do1(/* complete with necessary parameters*/)
{

}

void do2(/* complete with necessary parameters*/)
{

}
virtual void check(Holder* holder, int index){  };
public:
void Insert(Holder* holder, Element* element, int index)
{
    do1();
    check(holder, index);
    do2();    
};

class Expensive : BaseHolder
{
protected:
override void check(Holder* holder, int index)
{ 
    holder->ensureRange(index);
}
};

class Cheap : BaseHolder
{
};

This solution defines the check method only on an Expensive object, it has the most code reuse and it is definetly one of the cleanest approaches. Unfortunately this is about oop design more than about the performance where it is not the best, but you will have to think which is your priority, as you just concluded.

Graham
  • 7,431
  • 18
  • 59
  • 84
meJustAndrew
  • 6,011
  • 8
  • 50
  • 76
  • Are there any reasons for not making methods static? If there are no such reasons then why cant't they be just functions under namespaces? I.e. can you please add examples of usage to your answer? – mvidelgauz Aug 07 '16 at 08:53
  • I don't care about syntax or insignificant detail :) , but your solution suffer the same disadvantage as my first solution - impractical if do2() want local variable of do1(). – javaLover Aug 07 '16 at 08:53
  • 1
    @javaLover you could send these values as parameters, or hold them as class protected fields, so they could be used by `do2` too. – meJustAndrew Aug 07 '16 at 08:55
  • 1
    @javaLover thank you! These approaches will make your code more maintainable, but nothing compares as performance with a simple if check so I guess your second option is the best from this point of view – meJustAndrew Aug 07 '16 at 09:06
  • This looks nice, though I doubt it is better in performance. It introduces a lot of indirection and even virtual calls, the fast case will definitely be a few instructions (and cache-misses) slower than any of the original cases – JVApen Aug 07 '16 at 09:13
  • You are right @JVApen it's performance it's not the best, not even close to a single if statement check, but it would work close to the original one with some compiler optimisations. As stated in the question text yet, this is not the case to put the accent on maintainability, but on performance, so it is not the appropriate answer. – meJustAndrew Aug 07 '16 at 09:33
1

Technically, I'd prefer a solution like shown in mvidelgauz's answer, but the added arguments look bad when the function has to be actually called because of the not self-explaining literals true or false. So I'd combine three functions: an internal (private) function that provides the check flag as an argument, and two external functions that provide obvious names about the function, I'd prefer something like checkedInsert, uncheckedInsert. These public functions then will call the 4-argument implementation.

In the following code snippet reduced most of the parameters to make the solution most obvious. It should be seen as a fragment of a class definition:

public:
/// performs a fast insert without range checking. Range-check yourself before!
void checkedInsert(Element* element)
{
    insertWithCheckOption(element, true);
}

/// performs a range-checked insert
void uncheckedInsert(Element* element)
{
    insertWithCheckOption(element, false);
}

private:
/// implements insert with range check option
void insertWithCheckOption(Element* element, bool doRangeCheck)
{
    // ... do before code portion ...
    if (doRangeCheck) {
         // do expansive range checking  ...
    }
    // ... do after code portion ...
}

This solution provides both: the best user interface and a coherent implementation.
BTW: I really doubt that real performance issues will ever be caused by a single conditional check.

Wolf
  • 9,679
  • 7
  • 62
  • 108
  • 1
    _"but the added arguments look bad when the function has to be actually called because of the not self-explaining literals true or false"_ I totally agree with you! But won't it be simpler to just use `enum` (with very_long_extremely_descriptive_human_readable_values) instead of `bool` to improve readability? I think this is the classic example of the readability practice but I wanted to focus on the question. A lot of other considerations can still be added to this architectural question. Are we going to discuss all of them here? ))) – mvidelgauz Aug 07 '16 at 09:11
  • 1
    I try to avoid enums whenever possible (well, this should have a rethink after swithing to C++17), because of the problem to place the definition somewhere. Only now, I read that performance has the highest priority for the OP. But I leaned in lot's of bad experiences that readability should be the first in this list. So maybe my answer is useless in "this special case".(?) – Wolf Aug 07 '16 at 09:15
  • @mvidelgauz another point: I don't like many parameters, and third point: I don't like "just to add" something to the code: if there is a problem, I try to think also about changing the structure. – Wolf Aug 07 '16 at 09:16
  • 1
    _" I don't like "just to add" something to the code: if there is a problem, I try to think also about changing the structure"_ Sometimes this habit leads to over-design, doesn't it? – mvidelgauz Aug 07 '16 at 09:18
  • 1
    Thank Wolf, I start to understand your solution - focus on readability. Although the answer is not so useful for the scope question, it is very useful for me. "readability should be the first in this list" is a nice quote. :) – javaLover Aug 07 '16 at 09:20
  • @mvidelgauz no I do not change only because I think about changing ;) I tried to stress my dislike to fast additions of `if`s and `bool` switches that corrupt the code over time. – Wolf Aug 07 '16 at 09:35
0

As another alternative, you can simply use a default function argument, with a default value corresponding to no range check

void insert(Holder* holder, Element* element, int index, 
            bool performRangeCheck = false)
{
  /* do some complex thing 1 */

  if(performRangeCheck)
  {
    holder->ensureRange(index);
  }

  /* do some complex thing 2 */
}

// ...

insert(holder, element, 2);       // no range check
insert(holder, element, 2, true); // perform range check
dfrib
  • 70,367
  • 12
  • 127
  • 192