emacs-devel
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Re: Lisp-level macro to avoid excessive GC in memory-allocating code (wa


From: Ihor Radchenko
Subject: Re: Lisp-level macro to avoid excessive GC in memory-allocating code (was: Larger GC thresholds for non-interactive Emacs)
Date: Fri, 01 Jul 2022 15:52:53 +0800

Eli Zaretskii <eliz@gnu.org> writes:

> Please don't forget that GC doesn't only collects unused Lisp objects,
> it also does other useful memory-management related tasks.  It
> compacts buffer text and strings, and it frees unused slots in various
> caches (font cache, image cache, etc.).  You can find in the archives
> discussions where innocently-looking code could cause Emacs run out of
> memory because it used too many fonts without flushing the font cache
> (any program that works on the list of fonts returned by the likes of
> x-list-fonts is in danger of bumping into that).

Then, if we decide to implement the macro I am suggesting, such macro
should not affect memory allocation of such sensitive objects: font
cache, image cache, etc. Just "safe" memory allocations.

>> As one idea, a lisp program may mark some of the variables to be skipped
>> by GC and to not contribute to GC threshold checks (that is, allocating
>> memory into the marked variables will not increase the memory counter
>> used by GC).
>> 
>> WDYT?
>
> I'm not sure I understand how this idea can be implemented.  The
> counting of how much Lisp data since last GC was produced is done
> _before_ variables are bound to the produced data as values.  So by
> the time we know the data is bound to such "special" variables, it's
> already too late, and the only way to do what you suggest would be to
> increase consing_until_gc back after we realize this fact.  Which
> would mean computing how much consing was done for the value of these
> variables, and that would probably slow down the generation of Lisp
> data, wouldn't it?
>
> Or what am I missing?

We can do the following:

1. In addition to directly bumping the TOTAL counter of newly allocated
   memory, we can introduce a new LOCAL counter holding recent
   allocations within current sexp.
2. Every time we return from a sexp/self-quoting object into assignment,
   if we are inside the proposed macro and also assigning value to one
   of the pre-defined symbols, increase the upper LOCAL counter in the
   parent sexp. Otherwise, do not change the upper LOCAL counter.
3. Perform GC according to TOTAL-LOCAL threshold value.
4. When exiting the macro, set LOCAL to 0, unless inside another such
   macro.

Example (I intentionally avoid using dolist because macros using
temporary symbols complicate things):

(defvar lst-value)
(with-no-gc '(i return lst-value)
  (let (return (i 0))
    (while (< i 1000000)
      (setq return (cons i return))
      (make-string 1 ?a)
      (setq i (1+ i)))
    (setq lst-value return)))

Let TOTAL be the global counter and LOCAL be the local counter.

In the above code will:

1. Eval "0" to bind initial value of i. LOCAL=size_of_int;
   GLOBAL+=size_of_int. GC code will not count this allocation when
   deciding whether to perform GC.
2. Return 0 into (i 0) assignment and bump parent LOCAL because i is
   declared by the macro: LOCAL(let)+=LOCAL == size_of_int
3. Eval "1000000" in (< i 1000000); LOCAL=size_of_int; GLOBAL+=size_of_int.
4. Now, we are not inside an assignment, so LOCAL(<) == 0 (unchanged),
   TOTAL is increased, and TOTAL-LOCAL will be bumped by size_of_int.
5. Now, we are inside (while ...) and not inside assignment;
   LOCAL(while)+=LOCAL(<)  == size_of_int
5. Eval (cons i return); LOCAL(cons)=cons_size; GLOBAL+=cons_size;
6. Return the new cons into the assignment to return. Because return is
   declared by the macro, the outer value of local is bumped:
   LOCAL(while) == size_of_int+cons_size
7. Eval (make-string ...). This will allocate a new string;
   LOCAL(make-string)=string_length; GLOBAL+=string_length;
8. Return to (while ...) sexp; Because we are not inside the assignment
   LOCAL(while) is unchanged and the string allocation will contribute
   to the GC threshold.
9. Eval (1+ i), which will allocate a new integer;
   LOCAL(1+)=size_of_int; GLOBAL+=size_of_int;
10. The newly allocated integer is assigned to i symbol, declared by the
   macro; thus LOCAL(while)+=LOCAL(1+) ==
   size_of_int+cons_size+size_of_int; GLOBAL-LOCAL is unchanged and do
   not count towards next GC.
[second iteration]
11. Eval "1000000" in (< i 1000000), which will not allocate any memory
   because integers are immutable (AFAIK). GLOBAL and LOCAL remain
   unchanged.
... (all other iterations)
    LOCAL(while) == size_of_int * 1000000 + cons_size * (1- 1000000)
12. Return from (while ...). It is not an assignment, so
   LOCAL(let)+=LOCAL(while)
13. Assign lst_value, which will not allocate any extra memory.
14. Return from let: LOCAL(with-no-gc)+=LOCAL(let)
15. Return from with-no-gc. LOCAL=0 and the whole allocated object will
    now be able to contribute to the GC threshold, unless the example
   snippet is not by itself wrapped into parent with-no-gc call.
   In any case, while inside with-no-gc, only a single 1000000 symbol +
   all the string allocations can trigger GC. Assignments to i,result,
   and lst_value will only be counted upon exiting the macro.

Best,
Ihor
   





reply via email to

[Prev in Thread] Current Thread [Next in Thread]