From: Dmitry Antipov <dmantipov@yandex.ru>
To: Stefan Monnier <monnier@iro.umontreal.ca>
Cc: emacs-devel@gnu.org
Subject: Re: Proposal: block-based vector allocator
Date: Thu, 07 Jun 2012 14:03:00 +0400 [thread overview]
Message-ID: <4FD07C54.8030701@yandex.ru> (raw)
In-Reply-To: <jwvmx4gi7th.fsf-monnier+emacs@gnu.org>
[-- Attachment #1: Type: text/plain, Size: 940 bytes --]
On 06/06/2012 11:18 PM, Stefan Monnier wrote:
> The cost (CPU or memory, it doesn't matter much here, and we ignore
> fragmentation) of allocating a vector is something like:
> and CPU costs alike, ignores fragmentation):
> - outside of a vector-block: malloc + mem_node
> - inside a vector-block: vector-block + frac * (malloc + mem_node + a-bit-more)
> where "frac" is a fraction that's more or less vector-size /
> VECTOR_BLOCK_BYTES.
> So for small vectors, "frac" is small and since we expect the
> vector-block overhead to be significantly smaller than malloc+mem_node
> we win. But past a certain value of "frac", we're better off allocating
> outside of a vector-block. As I said, I don't know exactly where that
> threshold is, but using VECTOR_BLOCK_BYTES / 2 should be good enough.
OK. This is a bit more complicated since we need some tricks to
manage free space larger than (approx.) VECTOR_BLOCK_BYTES / 2.
Dmitry
[-- Attachment #2: vector_alloc.patch --]
[-- Type: text/plain, Size: 16011 bytes --]
=== modified file 'src/alloc.c'
--- src/alloc.c 2012-06-02 08:52:27 +0000
+++ src/alloc.c 2012-06-07 09:30:04 +0000
@@ -304,7 +304,9 @@
process, hash_table, frame, terminal, and window, but we never made
use of the distinction, so it only caused source-code complexity
and runtime slowdown. Minor but pointless. */
- MEM_TYPE_VECTORLIKE
+ MEM_TYPE_VECTORLIKE,
+ /* Special type to denote vector blocks. */
+ MEM_TYPE_VECTOR_BLOCK
};
static void *lisp_malloc (size_t, enum mem_type);
@@ -494,6 +496,11 @@
xsignal (Qnil, Vmemory_signal_data);
}
+/* A common multiple of the positive integers A and B. Ideally this
+ would be the least common multiple, but there's no way to do that
+ as a constant expression in C, so do the best that we can easily do. */
+#define COMMON_MULTIPLE(a, b) \
+ ((a) % (b) == 0 ? (a) : (b) % (a) == 0 ? (b) : (a) * (b))
#ifndef XMALLOC_OVERRUN_CHECK
#define XMALLOC_OVERRUN_CHECK_OVERHEAD 0
@@ -525,12 +532,8 @@
char c; \
}, \
c)
+
#ifdef USE_LSB_TAG
-/* A common multiple of the positive integers A and B. Ideally this
- would be the least common multiple, but there's no way to do that
- as a constant expression in C, so do the best that we can easily do. */
-# define COMMON_MULTIPLE(a, b) \
- ((a) % (b) == 0 ? (a) : (b) % (a) == 0 ? (b) : (a) * (b))
# define XMALLOC_HEADER_ALIGNMENT \
COMMON_MULTIPLE (1 << GCTYPEBITS, XMALLOC_BASE_ALIGNMENT)
#else
@@ -2928,17 +2931,334 @@
Vector Allocation
***********************************************************************/
-/* Singly-linked list of all vectors. */
+/* This value is balanced well enough to avoid too much internal overhead
+ for the most common cases; it's not required to be a power of two, but
+ it's expected to be a mult-of-ROUNDUP_SIZE (see below). */
-static struct Lisp_Vector *all_vectors;
+#define VECTOR_BLOCK_SIZE 4096
/* Handy constants for vectorlike objects. */
enum
{
header_size = offsetof (struct Lisp_Vector, contents),
- word_size = sizeof (Lisp_Object)
+ word_size = sizeof (Lisp_Object),
+ roundup_size = COMMON_MULTIPLE (sizeof (Lisp_Object),
+#ifdef USE_LSB_TAG
+ 8 /* Helps to maintain alignment constraints. */
+#else
+ 1 /* If alignment doesn't matter, should round up
+ to sizeof (Lisp_Object) at least. */
+#endif
+ )
};
+/* Round up X to nearest mult-of-ROUNDUP_SIZE,
+ assuming ROUNDUP_SIZE is a power of 2. */
+
+#define vroundup(x) (((x) + (roundup_size - 1)) & ~(roundup_size - 1))
+
+/* Rounding helps to maintain alignment constraints if USE_LSB_TAG. */
+
+#define VECTOR_BLOCK_BYTES (VECTOR_BLOCK_SIZE - vroundup (sizeof (void *)))
+
+/* Size of the smallest vector allocated from block. */
+
+#define VBLOCK_BYTES_MIN (vroundup (sizeof (struct Lisp_Vector)))
+
+/* Size of the largest vector allocated from block. */
+
+#define VBLOCK_BYTES_MAX \
+ (vroundup ((VECTOR_BLOCK_BYTES / 2) - sizeof (Lisp_Object)))
+
+/* We maintain one free list for each possible block-allocated
+ vector size, and this is the number of free lists we have. */
+
+#define VECTOR_MAX_FREE_LIST_INDEX \
+ ((VBLOCK_BYTES_MAX - VBLOCK_BYTES_MIN) / roundup_size + 1)
+
+/* When the vector is on a free list, vectorlike_header.SIZE is set to
+ this special value ORed with vector's memory footprint size. */
+
+#define VECTOR_FREE_LIST_FLAG \
+ ((~0) & ~(ARRAY_MARK_FLAG | PSEUDOVECTOR_FLAG \
+ | (VECTOR_BLOCK_SIZE - 1)))
+
+/* Common shortcut to advance vector pointer over a block data. */
+
+#define ADVANCE(v,nbytes) (struct Lisp_Vector *) ((char *) (v) + (nbytes))
+
+/* Common shortcut to calculate NBYTES-vector index in VECTOR_FREE_LISTS. */
+
+#define VINDEX(nbytes) (((nbytes) - VBLOCK_BYTES_MIN) / roundup_size)
+
+/* Common shortcut to setup vector on a free list. */
+
+#define SETUP_ON_FREE_LIST(v,nbytes,index) \
+ do { \
+ (v)->header.size = VECTOR_FREE_LIST_FLAG | (nbytes); \
+ eassert ((nbytes) % roundup_size == 0); \
+ (index) = VINDEX (nbytes); \
+ eassert ((index) < VECTOR_MAX_FREE_LIST_INDEX); \
+ (v)->header.next.vector = vector_free_lists[index]; \
+ vector_free_lists[index] = (v); \
+ } while (0)
+
+struct vector_block
+{
+ char data[VECTOR_BLOCK_BYTES];
+ struct vector_block *next;
+};
+
+/* Chain of vector blocks. */
+
+static struct vector_block *vector_blocks;
+
+/* Vector free lists, where NTH item points to a chain of free
+ vectors of the same NBYTES size, so NTH == VINDEX (NBYTES). */
+
+static struct Lisp_Vector *vector_free_lists[VECTOR_MAX_FREE_LIST_INDEX];
+
+/* Singly-linked list of large vectors. */
+
+static struct Lisp_Vector *large_vectors;
+
+/* The only vector with 0 slots, allocated from pure space. */
+
+static struct Lisp_Vector *zero_vector;
+
+/* Get a new vector block. */
+
+static struct vector_block *
+allocate_vector_block (void)
+{
+ struct vector_block *block;
+
+#ifdef DOUG_LEA_MALLOC
+ mallopt (M_MMAP_MAX, 0);
+#endif
+
+ block = xmalloc (sizeof (struct vector_block));
+ if (!block)
+ memory_full (sizeof (struct vector_block));
+
+#ifdef DOUG_LEA_MALLOC
+ mallopt (M_MMAP_MAX, MMAP_MAX_AREAS);
+#endif
+
+#if GC_MARK_STACK && !defined GC_MALLOC_CHECK
+ mem_insert (block->data, block->data + VECTOR_BLOCK_BYTES,
+ MEM_TYPE_VECTOR_BLOCK);
+#endif
+
+ block->next = vector_blocks;
+ vector_blocks = block;
+ return block;
+}
+
+/* Called once to initialize vector allocation. */
+
+static void
+init_vectors (void)
+{
+ zero_vector = (struct Lisp_Vector *)
+ pure_alloc (header_size, Lisp_Vectorlike);
+ zero_vector->header.size = 0;
+}
+
+/* If VECTOR is too large to join a free list at once,
+ this function splits it to smaller fragments. Next,
+ all fragments are set up to appropriate free lists. */
+
+static inline void
+setup_free_space (struct Lisp_Vector *vector)
+{
+ ptrdiff_t index, usedbytes, restbytes;
+
+ for (restbytes = vector->header.next.nbytes;
+ restbytes >= VBLOCK_BYTES_MIN; restbytes -= usedbytes)
+ {
+ eassert (restbytes % roundup_size == 0);
+ usedbytes = min (restbytes, VBLOCK_BYTES_MAX);
+
+ /* Do not create too small fragments. */
+ if (restbytes - usedbytes > 0)
+ while (restbytes - usedbytes < VBLOCK_BYTES_MIN)
+ usedbytes -= roundup_size;
+
+ SETUP_ON_FREE_LIST (vector, usedbytes, index);
+ vector = ADVANCE (vector, usedbytes);
+ }
+
+ /* Make sure all space is used. */
+ eassert (restbytes == 0);
+}
+
+/* Allocate vector from a vector block. */
+
+static struct Lisp_Vector *
+allocate_vector_from_block (size_t nbytes)
+{
+ struct Lisp_Vector *vector, *rest;
+ struct vector_block *block;
+ size_t index, restbytes;
+
+ eassert (VBLOCK_BYTES_MIN <= nbytes && nbytes <= VBLOCK_BYTES_MAX);
+ eassert (nbytes % roundup_size == 0);
+
+ /* First, try to allocate from a free list
+ containing vectors of the requested size. */
+ index = VINDEX (nbytes);
+ if (vector_free_lists[index])
+ {
+ vector = vector_free_lists[index];
+ vector_free_lists[index] = vector->header.next.vector;
+ vector->header.next.nbytes = nbytes;
+ return vector;
+ }
+
+ /* Next, check free lists containing larger vectors. Since
+ we will split the result, we should have remaining space
+ large enough to use for one-slot vector at least. */
+ for (index = VINDEX (nbytes + VBLOCK_BYTES_MIN);
+ index < VECTOR_MAX_FREE_LIST_INDEX; index++)
+ if (vector_free_lists[index])
+ {
+ /* This vector is larger than requested. */
+ vector = vector_free_lists[index];
+ vector_free_lists[index] = vector->header.next.vector;
+ vector->header.next.nbytes = nbytes;
+
+ /* Excess bytes are used for the smaller vector,
+ which should be set on an appropriate free list. */
+ restbytes = index * roundup_size + VBLOCK_BYTES_MIN - nbytes;
+ eassert (restbytes % roundup_size == 0);
+ rest = ADVANCE (vector, nbytes);
+ SETUP_ON_FREE_LIST (rest, restbytes, index);
+ return vector;
+ }
+
+ /* Finally, need a new vector block. */
+ block = allocate_vector_block ();
+
+ /* New vector will be at the beginning of this block. */
+ vector = (struct Lisp_Vector *) block->data;
+ vector->header.next.nbytes = nbytes;
+
+ /* The rest space from this block is free. */
+ rest = ADVANCE (vector, nbytes);
+ rest->header.next.nbytes = VECTOR_BLOCK_BYTES - nbytes;
+ setup_free_space (rest);
+
+ return vector;
+ }
+
+/* Return how many Lisp_Objects can be stored in V. */
+
+#define VECTOR_SIZE(v) ((v)->header.size & PSEUDOVECTOR_FLAG ? \
+ (PSEUDOVECTOR_SIZE_MASK & (v)->header.size) : \
+ (v)->header.size)
+
+/* Nonzero if VECTOR pointer is valid pointer inside BLOCK. */
+
+#define VECTOR_IN_BLOCK(vector,block) \
+ ((char *) (vector) <= (block)->data \
+ + VECTOR_BLOCK_BYTES - VBLOCK_BYTES_MIN)
+
+/* Reclaim space used by unmarked vectors. */
+
+static void
+sweep_vectors (void)
+{
+ struct vector_block *block = vector_blocks, **bprev = &vector_blocks;
+ struct Lisp_Vector *vector, *next, **vprev = &large_vectors;
+
+ total_vector_size = 0;
+ memset (vector_free_lists, 0, sizeof (vector_free_lists));
+
+ /* Looking through vector blocks. */
+
+ for (block = vector_blocks; block; block = *bprev)
+ {
+ int free_this_block = 0;
+
+ for (vector = (struct Lisp_Vector *) block->data;
+ VECTOR_IN_BLOCK (vector, block); vector = next)
+ {
+ if (VECTOR_MARKED_P (vector))
+ {
+ VECTOR_UNMARK (vector);
+ total_vector_size += VECTOR_SIZE (vector);
+ next = ADVANCE (vector, vector->header.next.nbytes);
+ }
+ else
+ {
+ ptrdiff_t nbytes;
+
+ if ((vector->header.size & VECTOR_FREE_LIST_FLAG)
+ == VECTOR_FREE_LIST_FLAG)
+ vector->header.next.nbytes =
+ vector->header.size & (VECTOR_BLOCK_SIZE - 1);
+ next = ADVANCE (vector, vector->header.next.nbytes);
+
+ /* While NEXT is not marked, try to coalesce with VECTOR,
+ thus making VECTOR of the largest possible size. */
+
+ while (VECTOR_IN_BLOCK (next, block))
+ {
+ if (VECTOR_MARKED_P (next))
+ break;
+ if ((next->header.size & VECTOR_FREE_LIST_FLAG)
+ == VECTOR_FREE_LIST_FLAG)
+ nbytes = next->header.size & (VECTOR_BLOCK_SIZE - 1);
+ else
+ nbytes = next->header.next.nbytes;
+ vector->header.next.nbytes += nbytes;
+ next = ADVANCE (next, nbytes);
+ }
+
+ /* Make sure resulting vector is valid. */
+ eassert (vector->header.next.nbytes % roundup_size == 0);
+
+ if (vector == (struct Lisp_Vector *) block->data
+ && !VECTOR_IN_BLOCK (next, block))
+ /* This block should be freed because all of it's
+ space was coalesced into the only free vector. */
+ free_this_block = 1;
+ else
+ setup_free_space (vector);
+ }
+ }
+
+ if (free_this_block)
+ {
+ *bprev = block->next;
+#if GC_MARK_STACK && !defined GC_MALLOC_CHECK
+ mem_delete (mem_find (block->data));
+#endif
+ xfree (block);
+ }
+ else
+ bprev = &block->next;
+ }
+
+ /* Sweep large vectors. */
+
+ for (vector = large_vectors; vector; vector = *vprev)
+ {
+ if (VECTOR_MARKED_P (vector))
+ {
+ VECTOR_UNMARK (vector);
+ total_vector_size += VECTOR_SIZE (vector);
+ vprev = &vector->header.next.vector;
+ }
+ else
+ {
+ *vprev = vector->header.next.vector;
+ lisp_free (vector);
+ }
+ }
+}
+
/* Value is a pointer to a newly allocated Lisp_Vector structure
with room for LEN Lisp_Objects. */
@@ -2960,8 +3280,19 @@
/* This gets triggered by code which I haven't bothered to fix. --Stef */
/* eassert (!handling_signal); */
+ if (len == 0)
+ return zero_vector;
+
nbytes = header_size + len * word_size;
- p = (struct Lisp_Vector *) lisp_malloc (nbytes, MEM_TYPE_VECTORLIKE);
+
+ if (nbytes <= VBLOCK_BYTES_MAX)
+ p = allocate_vector_from_block (vroundup (nbytes));
+ else
+ {
+ p = (struct Lisp_Vector *) lisp_malloc (nbytes, MEM_TYPE_VECTORLIKE);
+ p->header.next.vector = large_vectors;
+ large_vectors = p;
+ }
#ifdef DOUG_LEA_MALLOC
/* Back to a reasonable maximum of mmap'ed areas. */
@@ -2971,9 +3302,6 @@
consing_since_gc += nbytes;
vector_cells_consed += len;
- p->header.next.vector = all_vectors;
- all_vectors = p;
-
MALLOC_UNBLOCK_INPUT;
return p;
@@ -4072,7 +4400,35 @@
static inline int
live_vector_p (struct mem_node *m, void *p)
{
- return (p == m->start && m->type == MEM_TYPE_VECTORLIKE);
+ if (m->type == MEM_TYPE_VECTOR_BLOCK)
+ {
+ /* This memory node corresponds to a vector block. */
+ struct vector_block *block = (struct vector_block *) m->start;
+ struct Lisp_Vector *vector = (struct Lisp_Vector *) block->data;
+
+ /* P is in the block's allocation range. Scan the block
+ up to P and see whether P points to the start of some
+ vector which is not on a free list. FIXME: check whether
+ some allocation patterns (probably a lot of short vectors)
+ may cause a substantial overhead of this loop. */
+ while (VECTOR_IN_BLOCK (vector, block)
+ && vector <= (struct Lisp_Vector *) p)
+ {
+ if ((vector->header.size & VECTOR_FREE_LIST_FLAG)
+ == VECTOR_FREE_LIST_FLAG)
+ vector = ADVANCE (vector, (vector->header.size
+ & (VECTOR_BLOCK_SIZE - 1)));
+ else if (vector == p)
+ return 1;
+ else
+ vector = ADVANCE (vector, vector->header.next.nbytes);
+ }
+ }
+ else if (m->type == MEM_TYPE_VECTORLIKE && p == m->start)
+ /* This memory node corresponds to a vector
+ allocated outside of a block. */
+ return 1;
+ return 0;
}
@@ -4272,6 +4628,7 @@
break;
case MEM_TYPE_VECTORLIKE:
+ case MEM_TYPE_VECTOR_BLOCK:
if (live_vector_p (m, p))
{
Lisp_Object tem;
@@ -4705,6 +5062,7 @@
return live_float_p (m, p);
case MEM_TYPE_VECTORLIKE:
+ case MEM_TYPE_VECTOR_BLOCK:
return live_vector_p (m, p);
default:
@@ -6241,33 +6599,7 @@
}
}
- /* Free all unmarked vectors */
- {
- register struct Lisp_Vector *vector = all_vectors, *prev = 0, *next;
- total_vector_size = 0;
-
- while (vector)
- if (!VECTOR_MARKED_P (vector))
- {
- if (prev)
- prev->header.next = vector->header.next;
- else
- all_vectors = vector->header.next.vector;
- next = vector->header.next.vector;
- lisp_free (vector);
- vector = next;
-
- }
- else
- {
- VECTOR_UNMARK (vector);
- if (vector->header.size & PSEUDOVECTOR_FLAG)
- total_vector_size += PSEUDOVECTOR_SIZE_MASK & vector->header.size;
- else
- total_vector_size += vector->header.size;
- prev = vector, vector = vector->header.next.vector;
- }
- }
+ sweep_vectors ();
#ifdef GC_CHECK_STRING_BYTES
if (!noninteractive)
@@ -6404,7 +6736,6 @@
Vdead = make_pure_string ("DEAD", 4, 4, 0);
#endif
- all_vectors = 0;
ignore_warnings = 1;
#ifdef DOUG_LEA_MALLOC
mallopt (M_TRIM_THRESHOLD, 128*1024); /* trim threshold */
@@ -6417,6 +6748,7 @@
init_marker ();
init_float ();
init_intervals ();
+ init_vectors ();
init_weak_hash_tables ();
#ifdef REL_ALLOC
=== modified file 'src/lisp.h'
--- src/lisp.h 2012-05-30 19:23:37 +0000
+++ src/lisp.h 2012-06-07 05:19:08 +0000
@@ -916,11 +916,15 @@
{
ptrdiff_t size;
- /* Pointer to the next vector-like object. It is generally a buffer or a
+ /* When the vector is allocated from a vector block, NBYTES is used
+ if the vector is not on a free list, and VECTOR is used otherwise.
+ For large vector-like objects, BUFFER or VECTOR is used as a pointer
+ to the next vector-like object. It is generally a buffer or a
Lisp_Vector alias, so for convenience it is a union instead of a
pointer: this way, one can write P->next.vector instead of ((struct
Lisp_Vector *) P->next). */
union {
+ ptrdiff_t nbytes;
struct buffer *buffer;
struct Lisp_Vector *vector;
} next;
next prev parent reply other threads:[~2012-06-07 10:03 UTC|newest]
Thread overview: 62+ messages / expand[flat|nested] mbox.gz Atom feed top
2011-12-06 5:22 Proposal: block-based vector allocator Dmitry Antipov
2011-12-06 13:35 ` Stefan Monnier
2011-12-06 15:14 ` Dmitry Antipov
2011-12-06 19:39 ` Stefan Monnier
2011-12-07 5:05 ` Dmitry Antipov
2011-12-07 12:27 ` Carsten Mattner
2011-12-07 13:52 ` Stefan Monnier
2011-12-07 16:08 ` Dmitry Antipov
2011-12-07 16:30 ` Stefan Monnier
2011-12-08 8:50 ` Dmitry Antipov
2011-12-08 13:52 ` Stefan Monnier
2011-12-08 1:53 ` Stephen J. Turnbull
2011-12-08 4:41 ` Dmitry Antipov
2011-12-08 14:10 ` Stefan Monnier
2011-12-08 16:48 ` Dmitry Antipov
2011-12-08 19:58 ` Stefan Monnier
2011-12-09 7:32 ` Eli Zaretskii
2011-12-09 9:04 ` Dmitry Antipov
2011-12-09 14:05 ` Stefan Monnier
2011-12-09 16:15 ` Dmitry Antipov
2011-12-09 21:04 ` Stefan Monnier
2011-12-11 13:18 ` Dmitry Antipov
2011-12-12 3:07 ` Dmitry Antipov
2011-12-12 16:24 ` Stefan Monnier
2011-12-09 4:44 ` Stephen J. Turnbull
[not found] ` <jwvaa1yjs21.fsf-monnier+emacs@gnu.org>
2012-05-17 7:58 ` Dmitry Antipov
2012-05-18 17:40 ` Stefan Monnier
2012-05-21 12:19 ` Dmitry Antipov
2012-05-21 13:02 ` Andreas Schwab
2012-05-21 13:48 ` Dmitry Antipov
2012-05-21 15:07 ` Andreas Schwab
2012-05-22 5:23 ` Ken Raeburn
2012-05-21 20:12 ` Stefan Monnier
2012-05-22 8:24 ` Dmitry Antipov
2012-05-31 13:44 ` Dmitry Antipov
2012-05-31 15:43 ` Paul Eggert
2012-06-01 5:15 ` Dmitry Antipov
2012-06-01 5:44 ` Paul Eggert
2012-06-01 9:06 ` Dmitry Antipov
2012-06-01 17:36 ` Stefan Monnier
2012-06-02 0:32 ` Paul Eggert
2012-06-02 7:41 ` Eli Zaretskii
2012-06-03 6:49 ` Paul Eggert
2012-06-03 14:26 ` Eli Zaretskii
2012-05-31 21:16 ` Stefan Monnier
2012-06-01 7:34 ` Dmitry Antipov
2012-06-01 17:40 ` Stefan Monnier
2012-06-01 17:43 ` Stefan Monnier
2012-06-06 7:02 ` Dmitry Antipov
2012-06-06 13:13 ` Stefan Monnier
2012-06-06 14:58 ` Dmitry Antipov
2012-06-06 19:18 ` Stefan Monnier
2012-06-07 10:03 ` Dmitry Antipov [this message]
2012-06-07 14:07 ` Stefan Monnier
2012-06-08 5:50 ` Dmitry Antipov
2012-06-08 6:17 ` Stefan Monnier
2012-06-08 8:49 ` Dmitry Antipov
2012-06-08 8:53 ` Eli Zaretskii
2012-06-08 9:41 ` Eli Zaretskii
2012-06-08 10:00 ` Eli Zaretskii
2012-06-08 6:57 ` Eli Zaretskii
2012-06-08 6:38 ` Paul Eggert
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
List information: https://www.gnu.org/software/emacs/
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=4FD07C54.8030701@yandex.ru \
--to=dmantipov@yandex.ru \
--cc=emacs-devel@gnu.org \
--cc=monnier@iro.umontreal.ca \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
Code repositories for project(s) associated with this public inbox
https://git.savannah.gnu.org/cgit/emacs.git
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).