Showing posts with label profiling. Show all posts
Showing posts with label profiling. Show all posts

Sunday, February 13, 2022

Careful with profiling

Profiling

 The other day I was profiling a SSE optimized distance function and the more optimized form was 3 times as slow as the basic variant. The code was a bit like this (skipping the SSE variant):

template <typename T>
class Point
{
public:
   constexpr      Point     (T x, T y);

   constexpr T    GetX      () const;
   constexpr T    GetY      () const;

private:
   T              m_x;
   T              m_y;
};

template <typename T>
constexpr Point<T>::Point(T x, T y)
:  m_x(x)
,  m_y(y)
{
}

template <typename T>
constexpr T Point<T>::GetX() const
{
   return m_x;
}

template <typename T>
constexpr T Point<T>::GetY() const
{
   return m_y;
}

// explicit (DLL) instantiation
template class __declspec(dllexport) Point<double>;

double DistSqr(const Point<double>& rpt1, const Point<double>& rpt2)
{
    const double dx = rpt1.GetX() - rpt2.GetX();
    const double dy = rpt1.GetY() - rpt2.GetY();
   
    return (dx * dx) + (dy * dy);
}

It turned out that exported functions take a major performance hit; much larger than the two multiplications in 'DistSqr' function. The explicit exported template instantiation exports all functions; even constexpr and inline functions.The reason that calling exported function is slower:

  • invocation is a call instead of one simple memory read instruction
  • it suppresses other optimizations
  • just plain more instructions needed to transform data from 'Point' class to function

Removing the export the function was 3 times faster than without the exported attribute. The accessor functions 'GetX' are then inlined.

Lessons learned:

  • DLL and call invocations can harm performance
  • inspect the assembly

Note: accessor functions like 'GetX'  are prescribed by OOP but be aware of their potential performance cost.



Sunday, January 31, 2021

Multiplication and the optimizer

Test case

 the other day I was profiling the difference between floating point and integer multiplication. To test the multiplication contribution with the least amount of overhead contribution the functions did a number of multiplications. 

floating point

The test for floating point case was as follows:


double PrfMultiply(size_t nMax, double d, double dNumber)
{
   for (size_t n = 0; n != nMax; ++n)
   {
      d *= dNumber;
      d *= dNumber;
      d *= dNumber;
      d *= dNumber;

      d += (d < 100 ? 100.0 : 0);
   }

   return d;
}

  For x64 the Visual Studio compiler uses SSE instructions for floating point for numbers so it's no surprise then to see four (scalar) multiplication instructions in the generated code:


00007FF7C0D41700  mulsd       xmm7,xmm1  
00007FF7C0D41704  mulsd       xmm7,xmm1  
00007FF7C0D41708  mulsd       xmm7,xmm1  
00007FF7C0D4170C  mulsd       xmm7,xmm1 

Note: it's also remarkable that the compiler only uses the scalar SSE instruction and not invokes the packed variant (mulpd); From this table one can also see that (double) floating point multiplication lasts around 5 CPU cycles which makes it a fairly fast instruction

integer

  For integer the function looks almost the same:


size_t PrfIntegerMultiply(size_t nMax, size_t n, size_t nNumber)
{
   for (size_t n2 = 0; n2 != nMax; ++n2)
   {
      n *= nNumber;
      n *= nNumber;
      n *= nNumber;
      n *= nNumber;

      n -= (n > 1000 ? 995 : 0);
   }

   return n;
}

 It turns out that Visual Studio 2019 (in release mode) already optimized the four multiplication steps and coalesced them in one multiplication instruction (3^4 == 81 == 51h):


00007FF61C131840  imul        rbx,rbx,51h  

Conclusion

When profiling it's always advisable to look at the generated assembly code. Often the optimizer is smarter than you think or may even completely removes function invocation. This is especially the case with fixed predefined numbers and (static) functions in translation units.

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...