Subject: pdumper: reduce pdmp size for 64-bit systems

From: Pip Cet <[email-masked]>
Date: Fri, 21 Mar 2025 11:41:43 +0000

This is a wishlist item (with a PoC patch) for improving pdumper on
64-bit systems.

On such systems, we can save about 2 MB of the pdmp file (reducing it
from 14 MB to 12 MB without nativecomp, or 17 MB to 15 MB with
nativecomp) by storing dump-to-dump relocations for Lisp_Objects and
pointers in the heap image part of the dump, and omitting the
corresponding explicit 32-bit relocations in the relocation part of the
dump.

What makes this possible is that a 64-bit Lisp_Object or pointer is
usually large enough to contain two dump offsets: one to indicate which
object should be referred to after relocation, and another one linking
to the next in-place relocation, allowing us to find all of them by
walking the linked list.

There is no measurable performance difference on my system, though I
suspect that older systems will see performance gains because of the
reduction in pdmp size.  (In theory, it's easier to prefetch relocation
data with the old format, but in practice, using offsets rather than
pointers makes this impossible for GCC to achieve on current CPUs.)

Here's a proof of concept.  Is this worth pursuing?

My TODO list:

1. put the initial pointer into a relocation rather than the header
2. investigate possibility of a 32-bit version
3. compress dump-to-emacs relocations
4. use bitfields rather than direct bit manipulation
5. investigate reversing the chain so we don't go through the heap backwards

Note that the code is written to fall back to the old functions if a
relocation doesn't fit, so unusual dumping scenarios should continue to
work, they just won't benefit as much, and there will be fewer conflicts
with other changes to the pdumper code.

In particular, if there is still interest in working on a version of
pdumper which loads the dump at a fixed address and pre-fills the heap
image with the right pointers/Lisp_Objects (unexec-style), there'd be no
conflict: we just couldn't do both at the same time.

Similarly, the disabled DANGEROUS optimization in
dump_field_lv_or_rawptr would reduce the amount of memory saved.  It's
probably time to remove that code.

>From 50712c9a349e97a3f9fdd1daf08e5765799645b1 Mon Sep 17 00:00:00 2001
From: Pip Cet <[email-masked]>
Subject: [PATCH] Reduce pdump size by using in-place relocations on 64-bit
 systems

A 64-bit Lisp_Object is enough to store the dump offsets of both the
Lisp_Object that is meant to appear in a given location and the dump
offset of the next compressed relocation in the list.

* src/pdumper.c (dump_do_fixup): Compress a relocation plus a link to
the previous one into a single Lisp_Object, if it fits.
(dump_do_fixups):
(Fdump_emacs_portable): Keep track of the list of in-place fixup
relocations.
(dump_do_fixup_chain): Replace compressed in-place relocations by
their fixups.
(pdumper_load): Call 'dump_do_fixup_chain' before applying explicit
relocs.
---
 src/pdumper.c | 67 +++++++++++++++++++++++++++++++++++++++++++++------
 1 file changed, 59 insertions(+), 8 deletions(-)

diff --git a/src/pdumper.c b/src/pdumper.c
index de213130756..5ccac63637a 100644
--- a/src/pdumper.c
+++ b/src/pdumper.c
@@ -403,6 +403,9 @@ dump_fingerprint (FILE *output, char const *label,
 
   /* Offset of a vector of the dumped hash tables.  */
   dump_off hash_list;
+
+  /* Offset of the last relocation compressed into a Lisp_Object.  */
+  dump_off fixup_chain;
 };
 
 /* Double-ended singly linked list.  */
@@ -3991,10 +3994,11 @@ drain_reloc_list (struct dump_context *ctx,
   ctx->flags = old_flags;
 }
 
-static void
+static dump_off
 dump_do_fixup (struct dump_context *ctx,
                Lisp_Object fixup,
-               Lisp_Object prev_fixup)
+               Lisp_Object prev_fixup,
+	       dump_off prev_fixup_chain)
 {
   enum dump_fixup_type type
     = (enum dump_fixup_type) XFIXNUM (dump_pop (&fixup));
@@ -4013,6 +4017,7 @@ dump_do_fixup (struct dump_context *ctx,
   dump_seek (ctx, dump_fixup_offset);
   intptr_t dump_value;
   bool do_write = true;
+  bool do_link = false;
   switch (type)
     {
     case DUMP_FIXUP_LISP_OBJECT:
@@ -4051,7 +4056,18 @@ dump_do_fixup (struct dump_context *ctx,
           dump_value = dump_recall_object (ctx, arg);
           if (dump_value <= 0)
             error ("fixup object not dumped");
-          if (type == DUMP_FIXUP_LISP_OBJECT)
+	replace:
+	  if (sizeof (Lisp_Object) == 8
+	      && ctx->offset - prev_fixup_chain < 0x10000000
+	      && dump_value < 0x10000000)
+	    {
+	      uint64_t new_dump_value =
+		((dump_value << 36LL) + ((long long)((type == DUMP_FIXUP_LISP_OBJECT) ? (int) XTYPE (arg) : (int) 8) << 32LL) +
+		 (ctx->offset - prev_fixup_chain));
+	      do_link = true;
+	      dump_value = new_dump_value;
+	    }
+          else if (type == DUMP_FIXUP_LISP_OBJECT)
             dump_reloc_dump_to_dump_lv (ctx, ctx->offset, XTYPE (arg));
           else
             dump_reloc_dump_to_dump_ptr_raw (ctx, ctx->offset);
@@ -4062,8 +4078,7 @@ dump_do_fixup (struct dump_context *ctx,
          object.  It knows the exact location it wants, so just
          believe it.  */
       dump_value = dump_off_from_lisp (arg);
-      dump_reloc_dump_to_dump_ptr_raw (ctx, ctx->offset);
-      break;
+      goto replace;
     case DUMP_FIXUP_BIGNUM_DATA:
       {
         eassert (BIGNUMP (arg));
@@ -4081,11 +4096,13 @@ dump_do_fixup (struct dump_context *ctx,
     default:
       emacs_abort ();
     }
+  dump_off ret = do_link ? ctx->offset : prev_fixup_chain;
   if (do_write)
     dump_write (ctx, &dump_value, sizeof (dump_value));
+  return ret;
 }
 
-static void
+static dump_off
 dump_do_fixups (struct dump_context *ctx)
 {
   dump_off saved_offset = ctx->offset;
@@ -4094,13 +4111,15 @@ dump_do_fixups (struct dump_context *ctx)
 			      Qdump_emacs_portable__sort_predicate);
   Lisp_Object prev_fixup = Qnil;
   ctx->fixups = Qnil;
+  dump_off prev_fixup_chain = 0;
   while (!NILP (fixups))
     {
       Lisp_Object fixup = dump_pop (&fixups);
-      dump_do_fixup (ctx, fixup, prev_fixup);
+      prev_fixup_chain = dump_do_fixup (ctx, fixup, prev_fixup, prev_fixup_chain);
       prev_fixup = fixup;
     }
   dump_seek (ctx, saved_offset);
+  return prev_fixup_chain;
 }
 
 static void
@@ -4371,7 +4390,7 @@ DEFUN ("dump-emacs-portable",
   ctx->end_heap = ctx->offset;
 
   /* Make remembered modifications to the dump file itself.  */
-  dump_do_fixups (ctx);
+  dump_off fixup_chain = dump_do_fixups (ctx);
 
   drain_reloc_merger emacs_reloc_merger =
 #ifdef ENABLE_CHECKING
@@ -4410,6 +4429,7 @@ DEFUN ("dump-emacs-portable",
 
   /* Dump is complete.  Go back to the header and write the magic
      indicating that the dump is complete and can be loaded.  */
+  ctx->header.fixup_chain = fixup_chain;
   ctx->header.magic[0] = dump_magic[0];
   dump_seek (ctx, 0);
   dump_write (ctx, &ctx->header, sizeof (ctx->header));
@@ -5636,6 +5656,36 @@ dump_do_emacs_relocation (const uintptr_t dump_base,
     }
 }
 
+static void
+dump_do_fixup_chain (const struct dump_header *const header,
+		     const uintptr_t dump_base)
+{
+  const dump_off fixup_chain = header->fixup_chain;
+  dump_off curr_off = fixup_chain;
+  while (curr_off)
+    {
+      uint64_t dump_value;
+      memcpy (&dump_value,
+              dump_ptr (dump_base, curr_off),
+              sizeof (dump_value));
+      int type = (dump_value >> 32LL) & 15;
+      uintptr_t value = (dump_value >> 36LL) & 0xfffffff;
+      uintptr_t delta = dump_value & 0xfffffff;
+      void *obj_ptr = dump_ptr (dump_base, value);
+      Lisp_Object lv = Qnil;
+      if (type == 8)
+	lv = XIL ((uintptr_t)obj_ptr);
+      else if (type == Lisp_Symbol)
+	lv = make_lisp_symbol (obj_ptr);
+      else
+	lv = make_lisp_ptr (obj_ptr, type);
+      memcpy (dump_ptr (dump_base, curr_off),
+	      &lv,
+	      sizeof lv);
+      curr_off -= delta;
+    }
+}
+
 static void
 dump_do_all_emacs_relocations (const struct dump_header *const header,
 			       const uintptr_t dump_base)
@@ -5831,6 +5881,7 @@ pdumper_load (const char *dump_filename, char *argv0)
   dump_public.start = dump_base;
   dump_public.end = dump_public.start + dump_size;
 
+  dump_do_fixup_chain (header, dump_base);
   dump_do_all_dump_reloc_for_phase (header, dump_base, EARLY_RELOCS);
   dump_do_all_emacs_relocations (header, dump_base);
 
-- 
2.48.1