Repository: krakjoe/apcu
Pull request #562: Add defragmentation to apcu
Author: login: madmajestro
Created: 2025-05-23T18:26:24Z | Merged: 2025-05-31T19:19:25Z

This adds defragmentation logic to apcu, which is performed during the default_expunge operation. It works by shifting all allocated blocks to the left (low addresses), allowing all free blocks to be coalesced to one larger free block on the right side.


Diff:
diff --git a/TECHNOTES.txt b/TECHNOTES.txt
index e7cf642b..bd6a19dd 100644
--- a/TECHNOTES.txt
+++ b/TECHNOTES.txt
@@ -258,8 +258,8 @@ form of a quick-start guide to start hacking on APCu.
     /* {{{ struct definition: apc_cache_entry_t */
     typedef struct apc_cache_entry_t apc_cache_entry_t;
     struct apc_cache_entry_t {
-        zval val;                /* the zval copied at store time */
-        uintptr_t next;          /* offset in shm of next entry in linked list */
+        uintptr_t next;          /* offset to next entry (MUST BE THE 1st FIELD OF THE STRUCT!) */
+        uintptr_t prev;          /* offset to previous entry / head-pointer of the linked list */
         zend_long ttl;           /* the ttl on this specific entry */
         zend_long ref_count;     /* the reference count of this entry */
         zend_long nhits;         /* number of hits to this entry */
@@ -268,6 +268,7 @@ form of a quick-start guide to start hacking on APCu.
         time_t dtime;            /* time entry was removed from cache */
         time_t atime;            /* time entry was last accessed */
         zend_long mem_size;      /* memory used */
+        zval val;                /* the zval copied at store time */
         zend_string key;         /* entry key (MUST BE THE LAST FIELD OF THE STRUCT!) */
     };
     /* }}} */
diff --git a/apc_cache.c b/apc_cache.c
index 48bb863d..55765b0b 100644
--- a/apc_cache.c
+++ b/apc_cache.c
@@ -160,29 +160,65 @@ static zend_bool apc_cache_entry_expired(
 		|| apc_cache_entry_soft_expired(cache, entry, t);
 }
 
+/* apc_cache_wlocked_move_entry() is called during defragmentation, before an entry is moved to a new position. */
+static zend_bool apc_cache_wlocked_move_entry(apc_cache_t *cache, apc_cache_entry_t *old, apc_cache_entry_t *new) {
+	/* Check if the entry can be moved. */
+	if (old->ref_count > 0) {
+		return 0;
+	}
+
+	/* Change all references to this entry to the new position.
+	 * Since “next” is the 1st field of apc_cache_entry_t, the head pointer of the list
+	 * can be changed like a previous entry via ENTRYAT(old->prev)->next. */
+	ENTRYAT(old->prev)->next = ENTRYOF(new);
+	if (old->next) {
+		ENTRYAT(old->next)->prev = ENTRYOF(new);
+	}
+
+	return 1;
+}
+
+/* Inserts an entry into a linked list. The argument entry_offset must point either
+ * to entry->next of an existing entry or to the head pointer of a linked list. */
+static void apc_cache_wlocked_link_entry(apc_cache_t *cache, uintptr_t *entry_offset, apc_cache_entry_t *entry) {
+	entry->next = *entry_offset;
+	entry->prev = ENTRYOF(entry_offset);
+	*entry_offset = ENTRYOF(entry);
+	if (entry->next) {
+		ENTRYAT(entry->next)->prev = *entry_offset;
+	}
+}
+
+/* Removes an entry from a linked list. */
+static void apc_cache_wlocked_unlink_entry(apc_cache_t *cache, apc_cache_entry_t *entry) {
+	/* Since “next” is the 1st field of apc_cache_entry_t, the head pointer of the list
+	 * can be changed like a previous entry via ENTRYAT(entry->prev)->next. */
+	ENTRYAT(entry->prev)->next = entry->next;
+	if (entry->next) {
+		ENTRYAT(entry->next)->prev = entry->prev;
+	}
+}
+
 /* {{{ apc_cache_wlocked_remove_entry  */
-static void apc_cache_wlocked_remove_entry(apc_cache_t *cache, uintptr_t *entry_offset)
+static void apc_cache_wlocked_remove_entry(apc_cache_t *cache, apc_cache_entry_t *entry)
 {
-	apc_cache_entry_t *dead = ENTRYAT(*entry_offset);
-
-	/* unlink entry from list */
-	*entry_offset = dead->next;
+    /* unlink entry from list */
+	apc_cache_wlocked_unlink_entry(cache, entry);
 
 	/* adjust header info */
 	if (cache->header->mem_size)
-		cache->header->mem_size -= dead->mem_size;
+		cache->header->mem_size -= entry->mem_size;
 
 	if (cache->header->nentries)
 		cache->header->nentries--;
 
 	/* free entry if there are no references */
-	if (dead->ref_count <= 0) {
-		free_entry(cache, dead);
+	if (entry->ref_count <= 0) {
+		free_entry(cache, entry);
 	} else {
 		/* add to gc if there are still refs */
-		dead->dtime = time(0);
-		dead->next = cache->header->gc;
-		cache->header->gc = ENTRYOF(dead);
+		entry->dtime = time(0);
+		apc_cache_wlocked_link_entry(cache, &cache->header->gc, entry);
 	}
 }
 /* }}} */
@@ -220,7 +256,7 @@ static void apc_cache_wlocked_gc(apc_cache_t* cache)
 		}
 
 		/* set next and free current entry */
-		*entry_offset = entry->next;
+		apc_cache_wlocked_unlink_entry(cache, entry);
 		free_entry(cache, entry);
 	}
 }
@@ -356,7 +392,7 @@ static inline zend_bool apc_cache_wlocked_insert(
 				return 0;
 			}
 
-			apc_cache_wlocked_remove_entry(cache, entry_offset);
+			apc_cache_wlocked_remove_entry(cache, entry);
 			break;
 		}
 
@@ -365,7 +401,7 @@ static inline zend_bool apc_cache_wlocked_insert(
 		 * entries, so we don't always have to skip past a bunch of stale entries.
 		 */
 		if (apc_cache_entry_expired(cache, entry, t)) {
-			apc_cache_wlocked_remove_entry(cache, entry_offset);
+			apc_cache_wlocked_remove_entry(cache, entry);
 			continue;
 		}
 
@@ -374,8 +410,7 @@ static inline zend_bool apc_cache_wlocked_insert(
 	}
 
 	/* link in new entry */
-	new_entry->next = *entry_offset;
-	*entry_offset = ENTRYOF(new_entry);
+	apc_cache_wlocked_link_entry(cache, entry_offset, new_entry);
 
 	cache->header->mem_size += new_entry->mem_size;
 	cache->header->nentries++;
@@ -388,6 +423,7 @@ static void apc_cache_set_entry_values(apc_cache_entry_t *entry, const int32_t t
 {
 	entry->ttl = ttl;
 	entry->next = 0;
+	entry->prev = 0;
 	entry->ref_count = 0;
 	entry->nhits = 0;
 	entry->ctime = t;
@@ -696,7 +732,7 @@ static void apc_cache_wlocked_real_expunge(apc_cache_t* cache) {
 	for (i = 0; i < cache->nslots; i++) {
 		uintptr_t *entry_offset = &cache->slots[i];
 		while (*entry_offset) {
-			apc_cache_wlocked_remove_entry(cache, entry_offset);
+			apc_cache_wlocked_remove_entry(cache, ENTRYAT(*entry_offset));
 		}
 	}
 
@@ -754,9 +790,6 @@ PHP_APCU_API void apc_cache_default_expunge(apc_cache_t* cache, size_t size)
 		return;
 	}
 
-	/* gc */
-	apc_cache_wlocked_gc(cache);
-
 	/* smart > 1 increases the probability of a full cache wipe,
 	 * so expunge() is called less often when memory is low. */
 	size = (cache->smart > 0L) ? (size_t) (cache->smart * size) : size;
@@ -768,7 +801,7 @@ PHP_APCU_API void apc_cache_default_expunge(apc_cache_t* cache, size_t size)
 			apc_cache_entry_t *entry = ENTRYAT(*entry_offset);
 
 			if (apc_cache_entry_expired(cache, entry, t)) {
-				apc_cache_wlocked_remove_entry(cache, entry_offset);
+				apc_cache_wlocked_remove_entry(cache, entry);
 				continue;
 			}
 
@@ -777,15 +810,29 @@ PHP_APCU_API void apc_cache_default_expunge(apc_cache_t* cache, size_t size)
 		}
 	}
 
-	/* if the cache now has space, then reset last key */
-	if (apc_sma_get_avail_size(cache->sma, size)) {
-		/* wipe lastkey */
-		memset(&cache->header->lastkey, 0, sizeof(apc_cache_slam_key_t));
-	} else {
-		/* with not enough space left in cache, we are forced to expunge */
+	/* gc */
+	apc_cache_wlocked_gc(cache);
+
+	/* if all free blocks together do not provide enough memory, we immediately perform a real expunge */
+	if (!apc_sma_check_avail(cache->sma, size)) {
+		apc_cache_wlocked_real_expunge(cache);
+		apc_cache_wunlock(cache);
+		return;
+	}
+
+	/* run defragmentation to coalesce free blocks */
+	apc_sma_defrag(cache->sma, cache, (apc_sma_move_f)apc_cache_wlocked_move_entry);
+
+	/* if size bytes can't be allocated as a contiguous block after defragmentation, we do a real expunge */
+	if (!apc_sma_check_avail_contiguous(cache->sma, size)) {
 		apc_cache_wlocked_real_expunge(cache);
+		apc_cache_wunlock(cache);
+		return;
 	}
 
+	/* wipe lastkey */
+	memset(&cache->header->lastkey, 0, sizeof(apc_cache_slam_key_t));
+
 	apc_cache_wunlock(cache);
 }
 /* }}} */
@@ -965,7 +1012,7 @@ PHP_APCU_API zend_bool apc_cache_delete(apc_cache_t *cache, zend_string *key)
 		/* check for a match by hash and identifier */
 		if (apc_entry_key_equals(entry, key, h)) {
 			/* executing removal */
-			apc_cache_wlocked_remove_entry(cache, entry_offset);
+			apc_cache_wlocked_remove_entry(cache, entry);
 
 			apc_cache_wunlock(cache);
 			return 1;
diff --git a/apc_cache.h b/apc_cache.h
index e800854f..9fcf9ef9 100644
--- a/apc_cache.h
+++ b/apc_cache.h
@@ -49,8 +49,8 @@ struct apc_cache_slam_key_t {
 /* {{{ struct definition: apc_cache_entry_t */
 typedef struct apc_cache_entry_t apc_cache_entry_t;
 struct apc_cache_entry_t {
-	zval val;                /* the zval copied at store time */
-	uintptr_t next;          /* offset in shm of next entry in linked list */
+	uintptr_t next;          /* offset to next entry (MUST BE THE 1st FIELD OF THE STRUCT!) */
+	uintptr_t prev;          /* offset to previous entry / head-pointer of the linked list */
 	zend_long ttl;           /* the ttl on this specific entry */
 	zend_long ref_count;     /* the reference count of this entry */
 	zend_long nhits;         /* number of hits to this entry */
@@ -59,6 +59,7 @@ struct apc_cache_entry_t {
 	time_t dtime;            /* time entry was removed from cache */
 	time_t atime;            /* time entry was last accessed */
 	zend_long mem_size;      /* memory used */
+	zval val;                /* the zval copied at store time */
 	zend_string key;         /* entry key (MUST BE THE LAST FIELD OF THE STRUCT!) */
 };
 /* }}} */
diff --git a/apc_sma.c b/apc_sma.c
index d487a22d..8b00adc0 100644
--- a/apc_sma.c
+++ b/apc_sma.c
@@ -415,7 +415,11 @@ PHP_APCU_API size_t apc_sma_get_avail_mem(apc_sma_t* sma) {
 	return SMA_HDR(sma)->avail;
 }
 
-PHP_APCU_API zend_bool apc_sma_get_avail_size(apc_sma_t* sma, size_t size) {
+PHP_APCU_API zend_bool apc_sma_check_avail(apc_sma_t *sma, size_t size) {
+	return SMA_HDR(sma)->avail >= ALIGNWORD(size + ALIGNWORD(sizeof(block_t)));
+}
+
+PHP_APCU_API zend_bool apc_sma_check_avail_contiguous(apc_sma_t *sma, size_t size) {
 	size_t realsize = ALIGNWORD(size + ALIGNWORD(sizeof(block_t)));
 	sma_header_t *smaheader = SMA_HDR(sma);
 
@@ -442,6 +446,62 @@ PHP_APCU_API zend_bool apc_sma_get_avail_size(apc_sma_t* sma, size_t size) {
 	return 0;
 }
 
+PHP_APCU_API void apc_sma_defrag(apc_sma_t *sma, void *data, apc_sma_move_f move) {
+	sma_header_t *smaheader = SMA_HDR(sma);
+	block_t *cur = BLOCKAT(ALIGNWORD(sizeof(sma_header_t)) + ALIGNWORD(sizeof(block_t)));
+	block_t *first = BLOCKAT(ALIGNWORD(sizeof(sma_header_t)));
+
+	if (!SMA_LOCK(sma)) {
+		return;
+	}
+
+	/* empty the free list */
+	first->fnext = sma->size - ALIGNWORD(sizeof(block_t));
+	BLOCKAT(first->fnext)->fprev = OFFSET(first);
+
+	/* loop through all blocks */
+	while (cur->size != 0) {
+		/* continue until cur points to a free block */
+		if (!cur->fnext) {
+			cur = NEXT_SBLOCK(cur);
+			continue;
+		}
+
+		/* if cur is free, nxt must be an allocated block, since we never have two consecutive free blocks */
+		block_t *nxt = NEXT_SBLOCK(cur);
+
+		/* if nxt is the last block, or if nxt can't be moved, cur can't be combined with other free blocks */
+		if (nxt->size == 0 || !move(data, (char *)nxt + ALIGNWORD(sizeof(block_t)), (char *)cur + ALIGNWORD(sizeof(block_t)))) {
+			/* insert cur into the free list */
+			cur->fnext = first->fnext;
+			cur->fprev = OFFSET(first);
+			first->fnext = OFFSET(cur);
+			BLOCKAT(cur->fnext)->fprev = first->fnext;
+			cur->prev_size = 0;
+			nxt->prev_size = cur->size;
+
+			cur = NEXT_SBLOCK(nxt);
+			continue;
+		}
+
+		/* swap cur and nxt by moving nxt (incl. header) and initializing a new block header for cur behind it */
+		size_t free_size = cur->size;
+		memmove(cur, nxt, nxt->size);
+		cur->prev_size = 0;
+		cur = NEXT_SBLOCK(cur);
+		cur->size = free_size;
+		cur->fnext = 1; /* mark cur as free */
+
+		/* if the next block is also free, combine cur and nxt to one larger free block */
+		nxt = NEXT_SBLOCK(cur);
+		if (nxt->fnext) {
+			cur->size += nxt->size;
+		}
+	}
+
+	SMA_UNLOCK(sma);
+}
+
 /* }}} */
 
 /*
diff --git a/apc_sma.h b/apc_sma.h
index 7c15775f..a444c9d5 100644
--- a/apc_sma.h
+++ b/apc_sma.h
@@ -107,9 +107,27 @@ PHP_APCU_API void apc_sma_free_info(apc_sma_t* sma, apc_sma_info_t* info);
 PHP_APCU_API size_t apc_sma_get_avail_mem(apc_sma_t* sma);
 
 /*
-* apc_sma_api_get_avail_size will return true if at least size contiguous bytes are available to the sma
+* apc_sma_check_avail returns true if at least size bytes are available across all free blocks
 */
-PHP_APCU_API zend_bool apc_sma_get_avail_size(apc_sma_t* sma, size_t size);
+PHP_APCU_API zend_bool apc_sma_check_avail(apc_sma_t *sma, size_t size);
+
+/*
+* apc_sma_check_avail_contiguous returns true if at least size contiguous bytes can be allocated from the sma
+*/
+PHP_APCU_API zend_bool apc_sma_check_avail_contiguous(apc_sma_t *sma, size_t size);
+
+/*
+* apc_sma_defrag defragments the shared memory by shifting all allocated blocks to the left,
+* allowing all free blocks to be coalesced to one larger free block on the right side.
+*
+* The move() callback is called for each allocated block before it is moved. Therefore, move() can be used
+* to prepare for the move or to prevent the block from being moved by returning 0. The argument "data" is
+* passed as the first argument from apc_sma_defrag() to move(), while the old and the new address of the
+* allocation is passed as the 2nd and 3rd argument. The callback must not write to the new memory area
+* because the area is not yet allocated during the callback.
+*/
+typedef zend_bool (*apc_sma_move_f)(void *data, void *pointer_old, void *pointer_new);
+PHP_APCU_API void apc_sma_defrag(apc_sma_t *sma, void *data, apc_sma_move_f move);
 
 /* {{{ ALIGNWORD: pad up x, aligned to the system's word boundary */
 #define ALIGNWORD(x) ZEND_MM_ALIGNED_SIZE(x)
diff --git a/package.xml b/package.xml
index f9c90514..157585cc 100644
--- a/package.xml
+++ b/package.xml
@@ -75,6 +75,7 @@
     <file name="apc_025.phpt" role="test" />
     <file name="apc_099.phpt" role="test" />
     <file name="apc54_014.phpt" role="test" />
+    <file name="apc_defrag.phpt" role="test" />
     <file name="apc54_018.phpt" role="test" />
     <file name="apc_disabled.phpt" role="test" />
     <file name="apc_entry_001.phpt" role="test" />
diff --git a/tests/apc_defrag.phpt b/tests/apc_defrag.phpt
new file mode 100644
index 00000000..38a44dea
--- /dev/null
+++ b/tests/apc_defrag.phpt
@@ -0,0 +1,85 @@
+--TEST--
+Test defragmentation
+--SKIPIF--
+<?php
+require_once(__DIR__ . '/skipif.inc');
+if (!function_exists('apcu_inc_request_time')) die('skip APC debug build required');
+?>
+--INI--
+apc.enabled=1
+apc.enable_cli=1
+apc.use_request_time=1
+apc.shm_size=1M
+--FILE--
+<?php
+
+// fill_cache() fills the cache with small entries with alternating ttl=1 and ttl=3
+function fill_cache(): int {
+    $i = 0;
+    $min_entry_size = apcu_sma_info(true)['avail_mem'];
+    apcu_store(sprintf("ttl1_%010d", $i), $i, 1);
+    $min_entry_size -= apcu_sma_info(true)['avail_mem'];
+
+    while (apcu_sma_info(true)['avail_mem'] >= $min_entry_size) {
+        $i++;
+        apcu_store(sprintf("ttl3_%010d", $i), $i, 3);
+
+        if (apcu_sma_info(true)['avail_mem'] >= $min_entry_size) {
+            apcu_store(sprintf("ttl1_%010d", $i), $i, 1);
+        }
+    }
+
+    return $i;
+}
+
+// store the first entry with ttl=1, which causes all entries
+// behind this entry to be moved during defragmentation
+apcu_store("ttl1_int", 123, 1);
+
+// store entries of different datatypes with ttl=3, which must be present after expiration + defragmentation
+apcu_store("ttl3_int", 123456789, 3);
+apcu_store("ttl3_string", "abc", 3);
+apcu_store("ttl3_array", [1, 2, "a", "b"], 3);
+apcu_store("ttl3_object", (object) ["prop1" => "val1", "prop2" => 2], 3);
+
+// safe available memory for later comparison
+$avail_before_filled = apcu_sma_info(true)['avail_mem'];
+
+// fill cache with alternating ttl=1 + ttl=3 entries
+fill_cache();
+
+// ensure that cache is full
+var_dump(apcu_sma_info(true)['avail_mem']);
+
+// expire all ttl1_* entries
+apcu_inc_request_time(2);
+
+// this insertion should trigger an default_expunge which removes all ttl1_ entries and performs a defragmentation
+var_dump(apcu_store("large_entry", str_repeat('x', 1000), 1));
+
+// delete large_entry to be able to check the available memory in the next step
+var_dump(apcu_delete("large_entry"));
+
+// the defragmentation should have freed more than 50% of the filled memory, because "ttl1_int" was also freed
+var_dump(apcu_sma_info(true)['avail_mem'] > $avail_before_filled / 2);
+
+// after the default expunge, all ttl1_ entries should not be present anymore
+var_dump(apcu_fetch("ttl1_int") === false);
+
+// all ttl3_ entries must be present (and correct after defragmentation)
+var_dump(apcu_fetch("ttl3_int") === 123456789);
+var_dump(apcu_fetch("ttl3_string") === "abc");
+var_dump(apcu_fetch("ttl3_array") === [1, 2, "a", "b"]);
+var_dump(apcu_fetch("ttl3_object") == (object) ["prop1" => "val1", "prop2" => 2]);
+
+?>
+--EXPECT--
+float(0)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+bool(true)


Reviews:
user: login: nikic
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-31T15:16:42Z
user: login: madmajestro
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-31T18:39:33Z
user: login: nikic
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-31T19:19:08Z
user: login: nikic
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-31T19:19:15Z

Review comments:
diff_hunk: @@ -160,29 +160,65 @@ static zend_bool apc_cache_entry_expired(
 		|| apc_cache_entry_soft_expired(cache, entry, t);
 }
 
+/* apc_cache_wlocked_move_entry() is called during defragmentation, before an entry is moved to a new position. */
+static zend_bool apc_cache_wlocked_move_entry(apc_cache_t *cache, apc_cache_entry_t *old, apc_cache_entry_t *new) {
+	/* Check if the entry can be moved. */
+	if (old->ref_count > 0) {
+		return 0;
+	}
+
+	/* Change all references to this entry to the new position.
+	 * Since “next” is the 1st field of apc_cache_entry_t, the head pointer of the list
path: apc_cache.c
user: login: nikic
body: I don't really get why next being the 1st field matters here.
created_at: 2025-05-31T15:06:02Z
author_association: COLLABORATOR
diff_hunk: @@ -160,29 +160,65 @@ static zend_bool apc_cache_entry_expired(
 		|| apc_cache_entry_soft_expired(cache, entry, t);
 }
 
+/* apc_cache_wlocked_move_entry() is called during defragmentation, before an entry is moved to a new position. */
+static zend_bool apc_cache_wlocked_move_entry(apc_cache_t *cache, apc_cache_entry_t *old, apc_cache_entry_t *new) {
+	/* Check if the entry can be moved. */
+	if (old->ref_count > 0) {
+		return 0;
+	}
+
+	/* Change all references to this entry to the new position.
+	 * Since “next” is the 1st field of apc_cache_entry_t, the head pointer of the list
path: apc_cache.c
user: login: madmajestro
body: For example, if the first entry of a hash slot is moved during defragmentation, this code changes the "pointer" in the hash table, and it is accessed via `->next`, even though there is no data structure of type `apc_cache_entry_t` in the hash table, only an array of pointers. However, this access only works as long as `->next` is the first element of the data structure; otherwise, accessing the pointer in the hash table array would change other data.

Because this is a "trick/hack," I just wanted to point it out here. Alternatively, I could shorten/omit the comment here (and in apc_cache_wlocked_unlink_entry()), and/or describe it directly above the data structure in apc_cache.h.
created_at: 2025-05-31T18:39:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -160,29 +160,65 @@ static zend_bool apc_cache_entry_expired(
 		|| apc_cache_entry_soft_expired(cache, entry, t);
 }
 
+/* apc_cache_wlocked_move_entry() is called during defragmentation, before an entry is moved to a new position. */
+static zend_bool apc_cache_wlocked_move_entry(apc_cache_t *cache, apc_cache_entry_t *old, apc_cache_entry_t *new) {
+	/* Check if the entry can be moved. */
+	if (old->ref_count > 0) {
+		return 0;
+	}
+
+	/* Change all references to this entry to the new position.
+	 * Since “next” is the 1st field of apc_cache_entry_t, the head pointer of the list
path: apc_cache.c
user: login: nikic
body: Thanks, I get it now.
created_at: 2025-05-31T19:19:08Z
author_association: COLLABORATOR

PR comments:
user: login: madmajestro
created_at: 2025-05-23T18:36:46Z
body: I think I need some help here. I can't figure out why the pipeline is failing. The pipeline works in my repository.
author_association: CONTRIBUTOR
user: login: madmajestro
created_at: 2025-05-23T18:52:33Z
body: A few words about the defragmentation's performance:

In my (rough) tests so far, it ran faster than expected, always taking less than 60% of the time required by the default_expunge expiration loop. Furthermore, defragmentation had a positive effect on the insertion of new entries, as the subsequent search for free blocks worked faster.

Example:
I tested with 1 GB of shared memory and the smallest possible entry size with alternating TTL. In this case, the expiration loop took 0.345 seconds and the defragmentation took 0.193 seconds. These are typical values ​​I've observed in worst-case scenarios.
I used a Notebook with an AMD Ryzen 7 PRO 4750U with 32GB RAM for the test.
author_association: CONTRIBUTOR
user: login: madmajestro
created_at: 2025-05-23T20:42:11Z
body: > I think I need some help here. I can't figure out why the pipeline is failing. The pipeline works in my repository.

It seems that Appveyor had a temporary problem.
author_association: CONTRIBUTOR
user: login: madmajestro
created_at: 2025-05-27T17:05:26Z
body: I've expanded the test a bit so that there are now several entries with different data types that are moved during defragmentation. This allows the test to also check whether the move corrupts entries.
author_association: CONTRIBUTOR
user: login: madmajestro
created_at: 2025-05-30T14:48:19Z
body: Friendly ping.
author_association: CONTRIBUTOR
user: login: madmajestro
created_at: 2025-06-01T07:32:32Z
body: @nikic
Thanks a lot for your time!
author_association: CONTRIBUTOR