error / memory handling
Marc Glisse
marc.glisse at inria.fr
Fri Dec 26 13:58:13 UTC 2014
On Wed, 17 Dec 2014, Torbjörn Granlund wrote:
> I am not too fond of these global pointers. It would be better design
> to refer the memory handling and error handling functions from each GMP
> user variable, akin to object oriented languages' vtables. Except that
> this would make these small structures 3 times larger.
C++ containers have types like std::vector<type,allocator> so the
allocator is part of the type, and in many cases that avoids storing
anything at runtime. Obviously it makes it harder to keep precompiled code
in a library, and doesn't apply to people still coding in C.
> signed long _mp_size : 46;
> unsigned long _mp_alloc : 14; // 8-bit mantissa, 6-bit exponent
> unsigned long _mp_handler: 4;
> mp_limb_t _mp_d; // 64
Note that it is also possible to store extra data in _mp_d[-1]. IIRC
mpfr does that, and std::string in libstdc++ does it as well.
On Wed, 17 Dec 2014, Niels Möller wrote:
> tg at gmplib.org (Torbjörn Granlund) writes:
>
>> The error handler function would then use the knowledge of the
>> allocation functions to clean up the memory state, and probably longjump
>> to to deallocate stack.
>
> We've discussed such hacks for memory allocation before (but now I can't
> find the relevant posts...). To recap the idea:
>
> The "protected" code should take some input mpz variables and some
> output mpz_variables. The input variables are read only. All computation
> takes place in newly allocated temporaries, not touching the output
> variables. Only after the computation is complete and successful,
> results are assigned to the output variables using mpz_swap (which can
> never result in an allocation failure).
It is true that this is a common way to get exception safety. It is also
what a naive C++11 implementation using rvalue references would give
(c=a+b first creates a number for a+b then swaps it with c). Note that
this would completely kill performance in some applications, see below.
Most of the perf improvements I made to gmpxx consisted in doing more
operations in place, to avoid a new allocation. With a bit more care, it
should be possible to make operations safe without forcing an extra
allocation. Take mpz_add, I believe it is already safe, it first calls
realloc on the output (it is the user's responsibility to have a realloc
function that preserves memory when there is not enough space, if he wants
that). mpz_mul would require more reshuffling.
> Then needs a wrapper around the protected code, first adding a
> checkpoint in the allocation data structures, and a setjmp buffer. At
> any allocation failure during the computation, the allocation function
> should deallocate all live storage allocated after the checkpoint, and
> longjmp out, and the wrapper can then return some failure indication or
> raise a C++ exception or whatever. The same mechanism could clean up
> allocation after other types of failures.
A drawback of using setjmp/lonjmp is that they have a cost. C++ exceptions
on the other hand are designed to be almost free as long as nothing
throws. I would want to see benchmarks (on extremely small numbers, one or
two limbs) showing that it isn't too bad.
> Hmm. Maybe the key is to distinguish temporary allocations from more
> long-lived objects. That's the real point of the interface rules for the
> "protected" code above does: It ensures that *all* allocations are for
> temporaries).
If you consider that all allocations are for temporaries, you are also
giving a way to extend the life of a temporary, which makes temporary
memory not a stack anymore, so I believe you would still want 2 separate
allocation interfaces for real temporaries and for temporaries that will
become long-lived.
On Fri, 19 Dec 2014, Niels Möller wrote:
> The real problem is to do clean up temporary storage.
For mpn, the itch/scratch interface is a nice way to push the issue to the
user ;-) By the way, we could probably add __GMP_NOTHROW to more mpn
functions.
> Assuming the memory alllocation functions throw exception on errors,
> typical code catching that exception, cleaning up, and returning an
> error indication, would be something like
>
> bool
> foo (mpz_t out, const mpz_t in)
> {
> mpz_t tmp;
> memory_pool_push ();
> try {
> mpz_init (tmp);
> compute_something (tmp, in);
> mpz_set_parent (out, tmp);
> memory_pool_pop ();
> return true;
> }
> catch (...) {
> memory_pool_pop ();
> return false;
> }
> }
Side note: in practice, memory_pool_push and memory_pool_pop are in the
constructor and destructor of some object, so you don't have to repeat
memory_pool_pop on each path. And since in many cases you actually want to
propagate the exception, it also avoids the need for try/catch there.
> And with some care, I think this could be package reasonably nicely also
> with plain C, with some macro which both pushes a new memory pool and
> pushes a new jmpbuf for the exception handler. Some of this has the
> flavour of a general exception handling system for C, but that's
> unlikely to ever fly. So I think we should aim for internal gmp use, and
> application functions doing gmp calls only, no other allocations or
> anything else needing cleanup.
This is all well thought out, but somehow reinventing C++ exceptions in C
doesn't seem worth it to me...
On Wed, 24 Dec 2014, Niels Möller wrote:
> I agree a special stack allocator could have a place here (in
> particular, if for some reason running with a very small execution
> stack). And it could be done internally in gmp.
TMP_ALLOC is already a stack allocator, no?
> But also note that small allocations are typically done on the execution
> stack, with alloca, and that for larger allocations, the computational
> cost of computing the limbs stored there is vastly larger than the
> allocation overhead, making the latter negligible.
>
>> One of the difficulties with GMP is that the limb arrays associated
>> with mpz_t (and therefore mpq_t) objects *never* decrease in
>> size, unless the user explicitly calls mpz_realloc2().
>
> In case requiring explicit calls to mpz_realloc is difficult for real
> applications with "intermediate expression swell", we should consider
> doing that automatically in gmp, e.g., whenever size < alloc / 3 or so.
I have written applications where I was relying on this "expression
swell". With small numbers (just a few limbs), allocation dominates the
running time. In this case, I would keep reusing the same few mpq_t (there
were never that many in use at the same time) and rely on the fact that
after a few uses, reallocations became very rare (the total number of
reallocations in the application is actually bounded). I didn't need to
know how many mpq_t I would need nor how big they might be, just that the
product of those 2 numbers was bounded by some amount of memory I could
afford.
I agree this is a special case, but getting deallocations when the numbers
are small, or worse reallocating all the time with the swap trick, would
kill this application. I am sure I would eventually find other workarounds
though...
Since we are talking about allocations:
One thing I wanted to do at some point but that was complicated with the
current allocation interface:
struct mpz_struct_with_buffer : __mpz_struct { mp_limb_t buffer[10]; };
with an appropriate constructor that would by default have _mp_d point to
buffer. mpz_struct_with_buffer* is implicitly convertible to mpz_ptr so I
can use the gmp functions just fine, but realloc doesn't know where the
mpz_t is, so it cannot portably check if the pointer is to the local
buffer or if it can call free on it. So I had to waste one limb in the
buffer and every malloced region to store a bool saying if it came from
malloc or from an internal buffer.
Another thing was using alloca for small temporaries in gmpxx. When
someone computes a+b+c, the result of a+b is a temporary (sometimes I can
transform the operation to reuse the output variable, but not always), but
it is quite hard to use stack allocation there. The only ways I could
think of were:
a) precompute in gmpxx an upper bound on the size a number may need and
preallocate it appropriately with alloca (if small enough) before calling
the GMP functions;
b) using a separate stack (not the main execution stack) for those
allocations so I can control when to pop, changing the gmp allocation
functions around that operation. But that's a bit complicated, has some
overhead, and may not be a great fit for some uses. This is a special case
of Niels' pool idea.
--
Marc Glisse
More information about the gmp-devel
mailing list