Thursday, December 31, 2020

OOP and performance

Encapsulation and information hiding

  Encapsulation and information hiding are major concepts of object oriented programming (OOP). From a maintenance perspective it is beneficial not to be dependent or know about the implementation of an object. Not considering these aspects though can also harm the performance. Example given here may be obvious but is used for illustration purposes.

Example

  Consider the following sample class which uses a std::vector to store its data elements.The sample has the following member functions:

  • Add to add a datum
  • Clear to clear all contained data
  • Sum (or something else) to extract the data

#include <numeric>
#include <vector>

class Sample
{
public:
   void Add(double d)
   {
      m_vecData.push_back(d);
   }

   void Clear()
   {
      m_vecData.clear();
   }

   double Sum() const
   {
      return std::accumulate(m_vecData.cbegin(), m_vecData.cend(), 0.0);
   }

private:
   std::vector<double>  m_vecData;
};

Suppose there is some recorder class which produces sample data. There are a number of ways samples can be retrieved from the recorder:

  1. a function GetSample which returns the sample.
  2. a function FillSample which clear and fills a supplied sample.

class Recorder
{
public:
   Sample GetSample() const
   {
      Sample s;
      s.Add(m_dValue); // in real life the data comes from e.g. a buffer, file or socket

      return s;
   }

   void FillSample(Sample* pSample) const
   {
      pSample->Clear();
      pSample->Add(m_dValue);
   }
   
private:
   double   m_dValue = 1.0;
};

A client use is that many samples are extracted from the recorder in e.g. a loop.


// example 1:
double SampleGet(const Recorder& rRecorder, size_t nMax)
{
   double dTotal = 0.0;

   for (size_t n = 0; n != nMax; ++n)
   {
      const Sample s = rRecorder.GetSample();

      dTotal += s.Sum();
   }

   return dTotal;
}

// example 2:
double SampleGet(const Recorder& rRecorder, size_t nMax)
{
   double dTotal = 0.0;

   Sample s;

   for (size_t n = 0; n != nMax; ++n)
   {
      s = rRecorder.GetSample();

      dTotal += s.Sum();
   }

   return dTotal;
}

// example 3:
double SampleFill(const Recorder& rRecorder, size_t nMax)
{
   double dTotal = 0.0;

   Sample s;

   for (size_t n = 0; n != nMax; ++n)
   {
      rRecorder.FillSample(&s);

      dTotal += s.Sum();
   }

   return dTotal;
}

std::vector only reallocates memory when the capacity is too small. This steers the performance for a large part since memory allocations are performance wise relative heavy:

  1. using GetSample is the cleanest solution from a C++ perspective. Although NRVO may prevent superfluous sample copies, the sample (and thereby the vector) still needs to be created inside the function which may hamper the performance.
  2. using GetSample with the sample outside the loop is not much better.
  3. using FillSample is not the cleanest interface but is favorable from a performance perspective in this case since memory (re)allocation will only occur if the supplied Sample' vector cannot hold enough data elements

Above aspects are an issue due to the implementation details of the 'Sample' class and when performance considerations need to be weighted.

Counter argument

  The classical counter argument from an OO perspective would be that the interface should reflect and prevent bad client use. For this case one could delete the copy- and move constructors and assignment operators. Not sure if that is a good direction. In itself it's not a bad thing that samples get copied. After all they may be treated as value objects Also deleting these functions would prevent them of storing them in the preferred STL container std::vector.

    Conclusion

    As usual in engineering aspects have to be judged and balanced. The OO principles are good guidelines but it's good to know their limitations and break them when necessary.

    'Effective C++' (third edition) mentions a similar case in Item 26 'Postpone variable definitions as long as possible'.

    Wednesday, December 30, 2020

    inline in C++

    Keyword

     It's a common myth that the inline specifier in C++ is ignored by the compiler. While the standard states that it's only a hint for the compiler at least on Visual Studio 2019 the inlining process is influenced by the keyword.

    Test case

     For the test case the following class is used:

    
    struct Dummy
    {
        explicit      Dummy       (int n);
        
        int           Get         () const;  // defined in cpp
        int           GetInline   () const;
    
    private:
        int           m_n;
    };
    
    inline int Dummy::GetInline() const
    {
       return m_n;
    }
    
    

    Assembly

      The use of inlining by the compiler can be studied by looking at the produced assembly in release mode. Tip to use DebugBreak() under debugger to stop around call location.

    For normal function invocations it looks like:

    
    00007FF71ECF166C  lea         rcx,[rsp+30h]  
    00007FF71ECF1671  call        Dummy::Get (07FF71ECF1730h)  
    00007FF71ECF1676
    00007FF71ECF167C
    00007FF71ECF1680  add         ecx,eax  
    
    

     The inlined version looks like:

    
    00007FF71ECF1676
    00007FF71ECF167C  add         ecx,dword ptr [rsp+38h]  
    
    

     For function invocation a call instruction is issued while the inlined version uses an add of the memory content to register ecx. The assembly code for the non inlined function definition is a plain memory copy instruction so performance wise the call is just overhead.

    Results

     Tests take place on Visual Studio 2019 version 16.8.3 with a x64 build in release mode. The following cases are tested:

    1. use class inside a module
    2. use class inside a module with /LTCG (i.e. 'link time code generation')
    3. use exported class (i.e. the class is exported from a DLL and used in another module)

    The results are as follow:

    Case Get GetInline
    use inside a module not inlined inlined
    use inside a module with /LTCG inlined inlined
    use exported class not inlined inlined

    Conclusion

      The inline keyword is not ignored by Visual Studio 2019 and used as a hint. Recommendation to use it where applicable (e.g. one line 'getters' and performance intensive use cases). 

      The use of inline in a DLL context is food for discussion but it's okay as long as you rebuild the DLL (and its clients) when you change the source code of the DLL. MFC and boost are standard examples. 

     Other aspects not discussed here are use in (exported) templates and constexpr constructors (with noexcept).

    External links

    •  https://devblogs.microsoft.com/cppblog/inlining-decisions-in-visual-studio/

    Tuesday, December 29, 2020

    Defaults with invalid value

    Invalid values

    In code it's often natural to have (invalid) default values for initialization and (debug) checking. For example an enum in C++:

    
    enum Color
    {
        eClrUndefined = -1,
        eClrRed       = 0,
        eClrGreen,
    };
    
    class Ball
    {
        Color   m_eClr = eClrUndefined;
    };
    
    

    Identifier

    For object identifiers a long can be used. For example:

    
    using PersonId = long;
       
    constexpr PersonId g_idInvalid = -1;
    
    

    Code can test for validity of the id and checking against  an invalid value:

    
    const PersonId id = LookupPersonId(...);
    
    if (id != g_idInvalid)
    {
       //...
    }
    
    

    Alternative ways of signaling invalid values are e.g. std::optional but reserving a special value is more compact.

    Double

    For doubles it would be natural to use a NaN as invalid default value since NaN's stay invalid when used in (accidental) calculations. Suppose:

    
    #include <limits>
    
    using Distance = double;
    
    constexpr Distance g_dInvalid = std::numeric_limits<Distance>::signaling_NaN();
    
    

    Checking against invalid becomes a hindrance now since NaN's never check equal against another NaN or any other number. This gives already issues when defining the equality operator which could be solved by adding an extra case for NaN:

    
    #include <cmath>
    #include <limits>
    
    class Quantity
    {
       bool operator==(const Quantity& rRhs) const noexcept
       {
          if (std::isnan(m_dValue))
          {
             return std::isnan(rRhs.m_dValue);
          }
          else
          {
             return m_dValue == rRhs.m_dValue;
          }
        }
        
        double m_dValue = std::numeric_limits<double>::signaling_NaN();
    };
    
    

    Alternatives might therefore be more appropriate:

    • max or an other unreachable value
    • std::optional
    • use a function e.g. IsInvalidDistance which uses isnan.

    Note that exact equality checking of floating point numbers is room for another topic.

    Careful with std::ranges

    <ranges>   C++20 has added the the ranges library. Basically it works on ranges instead of iterators but added some subtle constraint...