Search code examples
c++performance

Optimizing conditional styling based on integer values in C++


I have an integer value representing different output messages to the user, such as error or success messages. Currently, I check if the integer is smaller than 100 to apply "The Error Style" font, and if it's greater than or equal to 100, I apply "The Success Style" font. I want to extend this logic to include additional styles for values over 1000 and so on.

Here's my current implementation:

if (m_errorNumber < 100) {
    errorColor.setColor(255, 0, 0, 255);
} else {
    errorColor.setColor(0, 255, 0, 255);
}

I'm considering optimizing this by checking the length of the integer instead. For example, if the length is between 1 and 2, or 3, or 4, etc., I would apply the corresponding style. I'm specifically seeking ways to improve performance and find the right case as quickly as possible.

My question is, how can I efficiently determine the appropriate case based on the integer value? Is there a more efficient way then If-Else-Checking?


Solution

  • So my detailed question is, how I can find the right case as fast as I can?

    You have very few "if smaller than ... else" rules, so performance is not an issue and there is no way, nor any need to increase speed. A series of ifs does the job well enough. Unless you've actually measured speed and detected a bottleneck, of course.

    From a computer-science point of view, however, it is possible to come up with a solution which offers better computational complexity, such that the number of necessary comparison operations will be proportionally reduced the larger your n gets, i.e. the more rules you have.


    Let's see. Your "if smaller than ... else" rules constitute certain ranges, and the bodies of your ifs all perform the same operation with different arguments.

    This scenario can be pictured as a map in which the end of each range is mapped to an argument set. In C++:

    struct ColorArguments { int a; int b; int c; int d; };
    std::map<int, ColorArguments> ranges;
    

    The map would then be filled as follows:

    ranges[100] = { 255, 0 , 0 , 255 };
    ranges[1000] = { 0, 255 , 0 , 255 };
    ranges[std::numeric_limits<int>::max()] = { 0, 0, 0, 255 };
    

    In fact, instead of filling it, you should rather initialise it and make it const:

    std::map<int, ColorArguments> const ranges = {
        { 100,                             { 255, 0   , 0, 255 } },
        { 1000,                            { 0,   255 , 0, 255 } },
        { std::numeric_limits<int>::max(), { 0,   0,    0, 255 } }
    };
    

    Finally, the upper_bound member function can be used to give you the appropriate ColorArguments objects. The function returns an iterator to the first element whose key is greater than the specified value.

    For example, in this map, searching for 500 would give you a pointer to the element with key 1000, because 1000 is the first key greater than 500.

    upper_bound has logarithmic complexity, which is pretty good and better than the linear complexity of your original if-else chains, notwithstanding potential compiler optimisations which may translate that linear code into something more complex.

    auto const iter = ranges.upper_bound(m_errorNumber);
    auto const& colors = iter->second;
    

    Note how using std::numeric_limits<int>::max() (the maximum possible int on your machine) as a key in the map avoids special handling for upper_bound returning ranges.end() (which works unless the input itself could be std::numeric_limits<int>::max(), in which case you do need some special handling).

    Here is a full example:

    #include <map>
    #include <limits>
    #include <iostream>
    
    struct ColorArguments { int a; int b; int c; int d; };
    
    void setColor(int a, int b, int c, int d)
    {
        std::cout << a << ", " << b << ", " << c << ", " << d << "\n";
    }
    
    int main()
    {
        std::map<int, ColorArguments> const ranges = {
            { 100,                             { 255, 0   , 0, 1 } },
            { 1000,                            { 0,   255 , 0, 2 } },
            { std::numeric_limits<int>::max(), { 0,   0,    0, 3 } }
        };
    
        int const errorNumber = 500;
    
        auto const iter = ranges.upper_bound(errorNumber);
    
        setColor(
            iter->second.a,
            iter->second.b,
            iter->second.c,
            iter->second.d
        );
    }
    

    Algorithmic complexity aside, this solution also turns static, hard-coded if-else logic into data, which can be modified or read dynamically at run-time. Even if you don't use it for your specific problem here, it's a technique to keep in mind.