1

When I print a cpp_int from Boost, it seems that the entire object is copied.

#include <iostream>
#include <boost/multiprecision/cpp_int.hpp>
using std::cout;

void* operator new(size_t size) {
    void *memory = malloc(size);
    cout << "New: " << memory << " " << size << "\n";
    return memory;
}

int main() {
    auto u = new boost::multiprecision::cpp_int("987654321");
    cout << "------\n";
    cout << *u << "\n";
}
New: 0x23d4e70 32
------
New: 0x23d52b0 31
987654321

The confusing thing is that the overload for printing is ostream& operator<<(ostream&, const T&), but passing *u into a function such as template <typename T> void cr(const T&) {} does not show any new memory allocation. I also tried u->str() but this also causes a 2nd memory allocation.

I also tried overloading the cout for the cpp_int:

std::ostream& operator <<(std::ostream& stream, const boost::multiprecision::cpp_int& mpi) {
    return stream << mpi.str();
}

but the result was the same. However, I am also surprised that this compiled since I had expected there to already be an overload. My assumption is that I may need to modify something more back-end.

How can I avoid this? I don't want to be copying and then deleting 30+ bytes every time I want to print a cpp_int.

If not, switching data type it not out of the question, as long as the interface is similar for minimal refactoring.

Doot
  • 555
  • 5
  • 15
  • Why are you using a `new` operator here? – user14717 Dec 01 '20 at 17:47
  • 1
    This is just a minimal example (but I'm using `new` in the actual, non-minimal program). Here, it serves no real purpose other than showing that the `new` overload actually works. If done on the stack, it's still the same problem though. – Doot Dec 01 '20 at 17:53
  • Note: matching `free` is missing because it didn't seem relevant since I was just trying to show unwanted memory allocated. – Doot Dec 02 '20 at 06:16

1 Answers1

4

The way you mismatched malloc/new is invoking UB (as ubsan+asan will readily tell you).

==32752==ERROR: AddressSanitizer: alloc-dealloc-mismatch (malloc vs operator delete
    #0 0x7fb58c15c407 in operator delete(void*, unsigned long) (/usr/lib/x86_64-lin
    #1 0x564b19759014 in __gnu_cxx::new_allocator<char>::deallocate(char*, unsigned
    #2 0x564b1974b8cb in std::allocator<char>::deallocate(char*, unsigned long) /us
    #3 0x564b1974b8cb in std::allocator_traits<std::allocator<char> >::deallocate(s
    #4 0x564b197478f4 in std::__cxx11::basic_string<char, std::char_traits<char>, s
    #5 0x564b19744f74 in std::__cxx11::basic_string<char, std::char_traits<char>, s
    #6 0x564b19741053 in std::__cxx11::basic_string<char, std::char_traits<char>, s
    #7 0x564b19744993 in std::ostream& boost::multiprecision::operator<< <boost::mu
    #8 0x564b1973da5a in main /home/sehe/Projects/stackoverflow/test.cpp:14
    #9 0x7fb58ab85bf6 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x21bf6
    #10 0x564b1973d669 in _start (/home/sehe/Projects/stackoverflow/sotest+0x4a669)

So let's focus on the claim:

#include <boost/multiprecision/cpp_int.hpp>
#include <iostream>

using Int = boost::multiprecision::cpp_int;

int main() {
    Int u("987654321");
    std::cout << u << "\n";
}

When we ask clang to follow the overload for operator<< it leads us here:

template <class Backend, expression_template_option ExpressionTemplates>
inline std::ostream& operator<<(std::ostream& os, const number<Backend, ExpressionTemplates>& r)
{
   std::streamsize d  = os.precision();
   std::string     s  = r.str(d, os.flags());
   std::streamsize ss = os.width();
   if (ss > static_cast<std::streamsize>(s.size()))
   {
      char fill = os.fill();
      if ((os.flags() & std::ios_base::left) == std::ios_base::left)
         s.append(static_cast<std::string::size_type>(ss - s.size()), fill);
      else
         s.insert(static_cast<std::string::size_type>(0), static_cast<std::string::size_type>(ss - s.size()), fill);
   }
   return os << s;
}

As you can see the number is taken by const-reference, so no copy is performed. There will be allocations for the buffers (in the str() implementation). I don't think the Multiprecision library is going to boast a highly optimized implementation of IO operations.

Memory Profiling

To see exactly what allocations are done where, I ran a debug build through Massif:

enter image description here

At the peak, the top allocations are:

99.97% (73,759B) (heap allocation functions) malloc/new/new[], --alloc-fns, etc.
->98.54% (72,704B) 0x50DDFA4: ??? (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.28
| ->98.54% (72,704B) 0x40108F1: _dl_init (dl-init.c:72)
|   ->98.54% (72,704B) 0x40010C8: ??? (in /lib/x86_64-linux-gnu/ld-2.27.so)
|     
->01.39% (1,024B) 0x58C526A: _IO_file_doallocate (filedoalloc.c:101)
| ->01.39% (1,024B) 0x58D5447: _IO_doallocbuf (genops.c:365)
|   ->01.39% (1,024B) 0x58D4566: _IO_file_overflow@@GLIBC_2.2.5 (fileops.c:759)
|     ->01.39% (1,024B) 0x58D2ABB: _IO_file_xsputn@@GLIBC_2.2.5 (fileops.c:1266)
|       ->01.39% (1,024B) 0x58C6A55: fwrite (iofwrite.c:39)
|         ->01.39% (1,024B) 0x516675A: std::basic_ostream<char, std::char_traits<ch
|           ->01.39% (1,024B) 0x10C85C: std::ostream& boost::multiprecision::operat
|             ->01.39% (1,024B) 0x10B6C6: main (test.cpp:8)
|               
->00.04% (31B) in 1 place, below massif's threshold (1.00%)

Sadly somehow I cannot make the threshold < 1% (this might be a documented limit).

What we can see is that though the 31B allocation happens somewhere, it is dwarfed by the file output buffer (1024B).

If we replace the output statement by just

return u.str().length();

you can still witness the 31B allocation, which does NOT match the size of the cpp_int type. Indeed, iff we were to copy THAT:

return std::make_unique<Int>(u)? 0 : 1;

THEN we see a 32B allocation instead:

->00.04% (32B) in 1 place, below massif's threshold (1.00%)

It is pretty clear that the cpp_int is not being copied, as makes sense.

sehe
  • 374,641
  • 47
  • 450
  • 633
  • Did some heap profiling to try and prove that the number isn't being copued. The actual allocations are under the reporting threshold, but we **can** deduce that the number is NOT being copied. – sehe Dec 02 '20 at 01:28
  • How to avoid the allocation? You can't because the interface is centered around returning a `std::string`. no allocator tweaking afforded. Sorry. You can elect to do some binary serialization based on the limbs manually. – sehe Dec 02 '20 at 01:33