Welcome to little lamb

Code » limb » commit 36cec36

Add nextsplit_{ae,buz,rabin} for content-based chunking

author Olivier Brunel
2023-02-15 18:05:58 UTC
committer Olivier Brunel
2023-02-15 18:05:58 UTC
parent a80d223784d8a210362816ff0af2b0b9901cf8bb

Add nextsplit_{ae,buz,rabin} for content-based chunking

These functions will determine the split for the next chunk in the given
data, using either Asymmetric Extremum Content Defined Chunking
Algorithm, which is notably extremely fast, or a rolling-hash algorithm based
on buzhash or Rabin fingerprint, which get better deduplication results, albeit
being slower.

Note that nextsplit_rabin() is actually a mix of two variants : with
chunk size below 128 KiB is uses the double-mask variant, from 128 KiB
above it uses the TTTD variant.

.gitignore +2 -0
doc/nextsplit_ae.3.md +51 -0
include/limb/int.h +5 -0
include/limb/nextsplit.h +11 -0
meta/AUTHORS +5 -0
meta/bins/mkrabintables +4 -0
meta/libs/limb +4 -0
project.mk +9 -0
src/nextsplit_ae.c +55 -0
src/nextsplit_buz.c +100 -0
src/nextsplit_rabin.c +60 -0
src/tools/mkrabintables.c +110 -0

diff --git a/.gitignore b/.gitignore
index fe2f03b..827b9aa 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,9 +3,11 @@
 /common.mk
 /config.mk
 /include/config.h
+/include/limb/rabin-tables.h
 *.o
 *.lo
 *.d
 liblimb.a
 liblimb.so
+/mkrabintables
 /skalibs
diff --git a/doc/nextsplit_ae.3.md b/doc/nextsplit_ae.3.md
new file mode 100644
index 0000000..29a4f01
--- /dev/null
+++ b/doc/nextsplit_ae.3.md
@@ -0,0 +1,51 @@
+% limb manual
+% nextsplit_ae(3)
+
+# NAME
+
+nextsplit_ae, nextsplit_buz, nextsplit_rabin - find the next split for
+variable-length chunks in a data stream
+
+
+# SYNOPSIS
+
+    #include <limb/nextsplit.h>
+
+```pre hl
+size_t nextsplit_ae(size_t <em>min</em>, size_t <em>avg</em>,
+                    const void *<em>data</em>, size_t <em>dlen</em>)
+size_t nextsplit_buz(size_t <em>min</em>, size_t <em>avg</em>,
+                     const void *<em>data</em>, size_t <em>dlen</em>)
+size_t nextsplit_rabin(size_t <em>min</em>, size_t <em>avg</em>,
+                       const void *<em>data</em>, size_t <em>dlen</em>)
+```
+
+# DESCRIPTION
+
+These functions are used for content-based chunking, in order to find the next
+breakpoint where to split the data stream into a chunk - useful for things such
+as data deduplication.
+
+Each of them will look into `data` (up to `dlen`) for position to split a chunk,
+which shall be at least `min` bytes long and with average/targeted length of
+`avg`. Failing to find one, they will return `dlen` for a full chunk - as such,
+`dlen` should be the maximum chunk size and not the total data length.
+
+The `nextsplit_ae`() function uses Asymmetric Extremum Content Defined Chunking
+Algorithm, which is notably extremely fast.
+
+The `nextsplit_buz`() function uses the buzhash rolling hash, which gets quite
+better deduplication results whilst remaining very fast (though not as fast as
+AE).
+
+The `nextsplit_rabin`() function uses a rolling-hash algorithm based on Rabin
+fingerprint, which tend to get slightly better deduplication results, albeit
+being slower.
+
+Note that it isn't uncommon for `nextsplit_buz`() to lead to better
+deduplication results (than both other alternatives).
+
+# RETURN VALUES
+
+All those functions will return the length of the next chunk from `data` as
+determined by their algorithm, or `dlen` when no such split could be determined.
diff --git a/include/limb/int.h b/include/limb/int.h
index df755ab..c730d6b 100644
--- a/include/limb/int.h
+++ b/include/limb/int.h
@@ -10,6 +10,11 @@ typedef uint16_t u16;
 typedef uint32_t u32;
 typedef uint64_t u64;
 
+#define U8_C(u)     UINT8_C(u)
+#define U16_C(u)    UINT16_C(u)
+#define U32_C(u)    UINT32_C(u)
+#define U64_C(u)    UINT64_C(u)
+
 extern int msb64(u64 val);
 
 #define u64_pack(val,dst)           uint64_pack((char *) (dst), val)
diff --git a/include/limb/nextsplit.h b/include/limb/nextsplit.h
new file mode 100644
index 0000000..4495901
--- /dev/null
+++ b/include/limb/nextsplit.h
@@ -0,0 +1,11 @@
+#ifndef LIMB_NEXTSPLIT_H
+#define LIMB_NEXTSPLIT_H
+
+#include <string.h> /* size_t */
+#include "limb/int.h"
+
+size_t nextsplit_ae(size_t min, size_t avg, const void *data, size_t dlen);
+size_t nextsplit_buz(size_t min, size_t avg, const void *data, size_t dlen);
+size_t nextsplit_rabin(size_t min, size_t avg, const void *data, size_t dlen);
+
+#endif /* LIMB_NEXTSPLIT_H */
diff --git a/meta/AUTHORS b/meta/AUTHORS
index 10768e8..7f8bd96 100644
--- a/meta/AUTHORS
+++ b/meta/AUTHORS
@@ -5,3 +5,8 @@ Contributors:
 * Aleksey Kravchenko <rhash.admin@gmail.com> [sha3]
 * Samuel Neves [blake3]
 * Jack O'Connor [blake3]
+* Jonas Borgström <jonas@borgstrom.se> & The Borg Collective [nextsplit_buz]
+* Yucheng Zhang [nextsplit_ae]
+
+Thanks to:
+* Min Fu
diff --git a/meta/bins/mkrabintables b/meta/bins/mkrabintables
new file mode 100644
index 0000000..6e3e1ca
--- /dev/null
+++ b/meta/bins/mkrabintables
@@ -0,0 +1,4 @@
+obj/tools/mkrabintables.o
+obj/msb64.o
+obj/put.o
+skalibs
diff --git a/meta/libs/limb b/meta/libs/limb
index 6e979ab..71f9d43 100644
--- a/meta/libs/limb
+++ b/meta/libs/limb
@@ -10,6 +10,10 @@ obj/msb64.o
 # {,un}pack u64
 obj/uint64_pack_trim.o
 obj/uint64_unpack_trim.o
+# content-based chunking
+obj/nextsplit_ae.o
+obj/nextsplit_buz.o
+obj/nextsplit_rabin.o
 # SHA3
 obj/sha3/byte_order.o
 obj/sha3/rhash_sha3_process_block.o
diff --git a/project.mk b/project.mk
index d20f193..7ed309a 100644
--- a/project.mk
+++ b/project.mk
@@ -1,5 +1,14 @@
 LIBS = limb
 
+TOOLS = mkrabintables
+
+CLEAN += include/limb/rabin-tables.h
+
+obj/nextsplit_rabin.o obj/nextsplit_rabin.lo: include/limb/rabin-tables.h
+
+include/limb/rabin-tables.h: mkrabintables
+	$(_GEN) ./mkrabintables > $@
+
 ifeq ($(BITS),64)
 BLAKE3_OPTIMIZ := obj/blake3/blake3_avx2_x86-64_unix.o \
 				  obj/blake3/blake3_avx512_x86-64_unix.o \
diff --git a/src/nextsplit_ae.c b/src/nextsplit_ae.c
new file mode 100644
index 0000000..166c031
--- /dev/null
+++ b/src/nextsplit_ae.c
@@ -0,0 +1,55 @@
+#include <endian.h>
+#include "limb/nextsplit.h"
+
+/* Author: Yucheng Zhang
+ * For more see paper: AE: An Asymmetric Extremum Content Defined Chunking
+ * Algorithm for Fast and Bandwidth-Efficient Data Deduplication
+ */
+
+#if __BYTE_ORDER == __BIG_ENDIAN
+# define GETLE32(u) (u)
+#else
+# define GETLE32(u) __builtin_bswap32(u)
+#endif
+
+static inline int
+cmp(const u8 *x, const u8 *y)
+{
+    int ret;
+    u32 a = GETLE32(* ((u32 *) x));
+    u32 b = GETLE32(* ((u32 *) y));
+    if (a > b)
+        ret = 1;
+    else
+        ret = -1;
+    return ret;
+}
+
+size_t
+nextsplit_ae(size_t min, size_t avg, const void *data, size_t dlen)
+{
+    if (dlen <= min) return dlen;
+
+    double e = 2.718281828;
+    int window_size = avg / (e - 1);
+    /* cur points to the current position
+     * max points to the position of max value
+     * end points to the end of buffer
+     */
+    const u8 *cur = (const u8 *) data + 1;
+    const u8 *max = (const u8 *) data;
+    const u8 *end = (const u8 *) data + dlen - sizeof(u32);
+
+    if (dlen <= window_size + sizeof(u32))
+        return dlen;
+
+    for ( ; cur <= end; ++cur) {
+        if (cmp(cur, max) < 0) {
+            max = cur;
+            continue;
+        }
+        if (cur == max + window_size)
+            return cur - (const u8 *) data;
+    }
+    return dlen;
+}
diff --git a/src/nextsplit_buz.c b/src/nextsplit_buz.c
new file mode 100644
index 0000000..6595741
--- /dev/null
+++ b/src/nextsplit_buz.c
@@ -0,0 +1,100 @@
+/* Original code from BorgBackup:
+ * Copyright (C) 2015-2022 The Borg Collective
+ * Copyright (C) 2010-2014 Jonas Borgström <jonas@borgstrom.se>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in
+ *    the documentation and/or other materials provided with the
+ *    distribution.
+ * 3. The name of the author may not be used to endorse or promote
+ *    products derived from this software without specific prior
+ *    written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+#include "limb/nextsplit.h"
+
+size_t
+nextsplit_buz(size_t min, size_t avg, const void *data, size_t dlen)
+{
+    u32 T[] =
+    {
+        0XE7F831EC, 0XF4026465, 0XAFB50CAE, 0X6D553C7A, 0XD639EFE3, 0X19A7B895, 0X9ABA5B21, 0X5417D6D4,
+        0X35FD2B84, 0XD1F6A159, 0X3F8E323F, 0XB419551C, 0XF444CEBF, 0X21DC3B80, 0XDE8D1E36, 0X84A32436,
+        0XBEB35A9D, 0XA36F24AA, 0XA4E60186, 0X98D18FFE, 0X3F042F9E, 0XDB228BCD, 0X096474B7, 0X5C20C2F7,
+        0XF9EEC872, 0XE8625275, 0XB9D38F80, 0XD48EB716, 0X22A950B4, 0X3CBAAEAA, 0XC37CDDD3, 0X8FEA6F6A,
+        0X1D55D526, 0X7FD6D3B3, 0XDAA072EE, 0X4345AC40, 0XA077C642, 0X8F2BD45B, 0X28509110, 0X55557613,
+        0XFFC17311, 0XD961FFEF, 0XE532C287, 0XAAB95937, 0X46D38365, 0XB065C703, 0XF2D91D0F, 0X92CD4BB0,
+        0X4007C712, 0XF35509DD, 0X505B2F69, 0X557EAD81, 0X310F4563, 0XBDDC5BE8, 0X9760F38C, 0X701E0205,
+        0X00157244, 0X14912826, 0XDC4CA32B, 0X67B196DE, 0X5DB292E8, 0X8C1B406B, 0X01F34075, 0XFA2520F7,
+        0X73BC37AB, 0X1E18BC30, 0XFE2C6CB3, 0X20C522D0, 0X5639E3DB, 0X942BDA35, 0X899AF9D1, 0XCED44035,
+        0X98CC025B, 0X255F5771, 0X70FEFA24, 0XE928FA4D, 0X2C030405, 0XB9325590, 0X20CB63BD, 0XA166305D,
+        0X80E52C0A, 0XA8FAFE2F, 0X1AD13F7D, 0XCFAF3685, 0X6C83A199, 0X7D26718A, 0XDE5DFCD9, 0X79CF7355,
+        0X8979D7FB, 0XEBF8C55E, 0XEBE408E4, 0XCD2AFFBA, 0XE483BE6E, 0XE239D6DE, 0X5DC1E9E0, 0X0473931F,
+        0X851B097C, 0XAC5DB249, 0X09C0F9F2, 0XD8D2F134, 0XE6F38E41, 0XB1C71BF1, 0X52B6E4DB, 0X07224424,
+        0X6CF73E85, 0X4F25D89C, 0X782A7D74, 0X10A68DCD, 0X3A868189, 0XD570D2DC, 0X69630745, 0X9542ED86,
+        0X331CD6B2, 0XA84B5B28, 0X07879C9D, 0X38372F64, 0X7185DB11, 0X25BA7C83, 0X01061523, 0XE6792F9F,
+        0XE5DF07D1, 0X4321B47F, 0X7D2469D8, 0X1A3A4F90, 0X48BE29A3, 0X669071AF, 0X8EC8DD31, 0X0810BFBF,
+        0X813A06B4, 0X68538345, 0X65865DDC, 0X43A71B8E, 0X78619A56, 0X5A34451D, 0X5BDAA3ED, 0X71EDC7E9,
+        0X17AC9A20, 0X78D10BFA, 0X6C1E7F35, 0XD51839D9, 0X240CBC51, 0X33513CC1, 0XD2B4F795, 0XCCAA8186,
+        0X0BABE682, 0XA33CF164, 0X18C643EA, 0XC1CA105F, 0X9959147A, 0X6D3D94DE, 0X0B654FBE, 0XED902CA0,
+        0X7D835CB5, 0X99BA1509, 0X6445C922, 0X495E76C2, 0XF07194BC, 0XA1631D7E, 0X677076A5, 0X89FFFE35,
+        0X1A49BCF3, 0X8E6C948A, 0X0144C917, 0X8D93AEA1, 0X16F87DDF, 0XC8F25D49, 0X1FB11297, 0X27E750CD,
+        0X2F422DA1, 0XDEE89A77, 0X1534C643, 0X457B7B8B, 0XAF172F7A, 0X6B9B09D6, 0X33573F7F, 0XF14E15C4,
+        0X526467D5, 0XAF488241, 0X87C3EE0D, 0X33BE490C, 0X95AA6E52, 0X43EC242E, 0XD77DE99B, 0XD018334F,
+        0X5B78D407, 0X498EB66B, 0XB1279FA8, 0XB38B0EA6, 0X90718376, 0XE325DEE2, 0X8E2F2CBA, 0XCAA5BDEC,
+        0X9D652C56, 0XAD68F5CB, 0XA77591AF, 0X88E37EE8, 0XF8FAA221, 0XFCBBBE47, 0X4F407786, 0XAF393889,
+        0XF444A1D9, 0X15AE1A2F, 0X40AA7097, 0X6F9486AC, 0X29D232A3, 0XE47609E9, 0XE8B631FF, 0XBA8565F4,
+        0X11288749, 0X46C9A838, 0XEB1B7CD8, 0XF516BBB1, 0XFB74FDA0, 0X010996E6, 0X4C994653, 0X1D889512,
+        0X53DCD9A3, 0XDD074697, 0X1E78E17C, 0X637C98BF, 0X930BB219, 0XCF7F75B0, 0XCB9355FB, 0X9E623009,
+        0XE466D82C, 0X28F968D3, 0XFEB385D9, 0X238E026C, 0XB8ED0560, 0X0C6A027A, 0X3D6FEC4B, 0XBB4B2EC2,
+        0XE715031C, 0XEDED011D, 0XCDC4D3B9, 0XC456FC96, 0XDD0EEA20, 0XB3DF8EC9, 0X12351993, 0XD9CBB01C,
+        0X603147A2, 0XCF37D17D, 0XF7FCD9DC, 0XD8556FA3, 0X104C8131, 0X13152774, 0XB4715811, 0X6A72C2C9,
+        0XC5AE37BB, 0XA76CE12A, 0X8150D8F3, 0X2EC29218, 0XA35F0984, 0X48C0647E, 0X0B5FF98C, 0X71893F7B
+    };
+    u32 mask = (1 << (msb64(avg) - 1)) - 1;
+
+#define WINDOW_SIZE 2047
+#define BARREL_SHIFT(v, shift) ( ((v) << (shift)) | ((v) >> ((32 - (shift)) & 0x1F)) )
+
+    const u8 *d = data;
+    u32 i, sum, imod;
+
+    if (dlen <= min + WINDOW_SIZE) return dlen;
+
+    sum = 0;
+    for (i = WINDOW_SIZE - 1; i > 0; --i) {
+        imod = i & 0x1F;
+        sum ^= BARREL_SHIFT(T[*d], imod);
+        ++d;
+    }
+    sum ^= T[*d];
+
+    d = data;
+    for (i = WINDOW_SIZE; i < dlen - WINDOW_SIZE; ++i) {
+        sum = BARREL_SHIFT(sum, 1) ^ BARREL_SHIFT(T[*d], 0x1F) ^ T[d[WINDOW_SIZE]];
+        if ((sum & mask) == 0) break;
+        ++d;
+    }
+    if (i >= dlen - WINDOW_SIZE)
+        i = dlen;
+
+    return i;
+}
diff --git a/src/nextsplit_rabin.c b/src/nextsplit_rabin.c
new file mode 100644
index 0000000..99c7c8c
--- /dev/null
+++ b/src/nextsplit_rabin.c
@@ -0,0 +1,60 @@
+#include "limb/nextsplit.h"
+#include "limb/rabin-tables.h"
+
+/* This is based on rabin fingerprint, using a miw of two different variants :
+ * - for avg < 128 KiB the double-mask variant, which uses a large mask when avg
+ *   hasn't yet been reached, then a smaller mask afterwards
+ * - from avg >= 128 KiB the TTTD variant, which also uses two masks, the
+ *   "standard" one and a smaller/fallback one used only when the first one
+ *   didn't find a split.
+ * Using those two (in that manner) is just the result of some testing, in an
+ * attempt to get the best deduplication results.
+ */
+
+size_t
+nextsplit_rabin(size_t min, size_t avg, const void *data, size_t dlen)
+{
+    if (dlen <= min) return dlen;
+
+    u8 window[RABIN_WINDOW_SIZE] = { 0 };
+    const u8 *b = data;
+    size_t wpos = 0, pos = min, last = 0;
+    u64 fp = 0;
+    u64 mask, lrg_mask;
+    int tttd = avg >= (128 << 10);
+
+    if (tttd)
+        mask = (1 << (msb64(avg    ) - 1)) - 1;
+    else
+        mask = (1 << (msb64(avg * 2) - 1)) - 1;
+    lrg_mask = (1 << (msb64(avg / 2) - 1)) - 1;
+
+#define TTTD_BREAK      0x78
+#define APPEND8(p,m)    (((p) << 8) | m) ^ rabin_tables.T[(p) >> 55]
+    for (;;) {
+        u8 old = window[wpos];
+        window[wpos] = b[pos];
+        fp = APPEND8(fp ^ rabin_tables.U[old], window[wpos]);
+        if (++wpos == sizeof(window)) wpos = 0;
+
+        if (pos == dlen) {
+            if (last) return last;
+            return pos;
+        }
+        if (pos >= min) {
+            if (tttd) {
+                if ((fp & lrg_mask) == TTTD_BREAK) {
+                    if ((fp & mask) == TTTD_BREAK)
+                        return pos;
+                    last = pos;
+                }
+            } else if (pos < avg) {
+                if ((fp & mask) == mask)
+                    return pos;
+            } else if ((fp & lrg_mask) == lrg_mask) {
+                return pos;
+            }
+        }
+        ++pos;
+    }
+}
diff --git a/src/tools/mkrabintables.c b/src/tools/mkrabintables.c
new file mode 100644
index 0000000..56ae8fe
--- /dev/null
+++ b/src/tools/mkrabintables.c
@@ -0,0 +1,110 @@
+#include <skalibs/uint16.h>
+#include "limb/int.h"
+#include "limb/output.h"
+
+const char *PROG = "mkrabintables";
+
+#define RABIN_FINGERPRINT 0xBFE6B8A5BF378D83ULL
+
+struct rabin {
+    u16 window_size;
+    u64 T[256];
+    u64 U[256];
+};
+
+#define MSB64 (1ULL << 63)
+
+static u64
+polymod(u64 nh, u64 nl, u64 d)
+{
+    int i;
+    int k = msb64(d) - 1;
+    d <<= 63 - k;
+
+    if (nh) {
+        if (nh & MSB64)
+            nh ^= d;
+        for (i = 62; i >= 0; --i)
+            if (nh & ((u64) 1) << i) {
+                nh ^= d >> (63 - i);
+                nl ^= d << (i + 1);
+            }
+    }
+    for (i = 63; i >= k; --i)
+        if (nl & 1LL << i)
+            nl ^= d >> (63 - i);
+    return nl;
+}
+
+static u64
+polymmult(u64 x, u64 y, u64 d)
+{
+    u64 h = 0, l = 0;
+    if (x & 1)
+        l = y;
+    for (int i = 1; i < 64; ++i)
+        if (x & (1LL << i)) {
+            h ^= y >> (64 - i);
+            l ^= y << i;
+        }
+    return polymod(h, l, d);
+}
+
+static void
+init_tables(struct rabin *r)
+{
+    int i;
+    int xshift = msb64(RABIN_FINGERPRINT) - 1;
+
+    u64 T1 = polymod(0, 1LL << xshift, RABIN_FINGERPRINT);
+    for (i = 0; i < 256; ++i) {
+        r->T[i] = polymmult(i, T1, RABIN_FINGERPRINT) | ((u64) i << xshift);
+    }
+
+#define SHIFT           55  /* == xshift - 8 */
+#define APPEND8(p,m)    (((p) << 8) | m) ^ r->T[(p) >> SHIFT]
+    u64 sizeshift = 1;
+    for (i = 1; i < r->window_size; ++i)
+        sizeshift = APPEND8(sizeshift, 0);
+
+    for (i = 0; i < 256; ++i)
+        r->U[i] = polymmult(i, sizeshift, RABIN_FINGERPRINT);
+}
+
+#include <stdio.h>
+#include <inttypes.h>
+
+int
+main(int argc, char **argv)
+{
+    struct rabin r = { .window_size = 32, };
+
+    if (argc == 2) {
+        if (!uint160_scan(argv[1], &r.window_size))
+            retw(1, "invalid window size");
+    } else if (argc != 1) {
+        dieusage(1, "[WINDOWSIZE]");
+    }
+
+    init_tables(&r);
+
+    printf( "#ifndef LIMB_RABIN_TABLES_H\n"
+            "#define LIMB_RABIN_TABLES_H\n"
+            "\n"
+            "#include \"limb/int.h\"\n"
+            "\n"
+            "#define RABIN_WINDOW_SIZE %u\n"
+            "\n"
+            "static struct {\n"
+            "	u64 T[256];\n"
+            "	u64 U[256];\n"
+            "} rabin_tables = { {\n", r.window_size);
+    for (int i = 0; i < 256; ++i)
+        printf("U64_C(%" PRIu64 ")%s", r.T[i], (i == 255) ? "" : ((i + 1) % 4) ? ", " : ",\n");
+    printf("\n}, {\n");
+    for (int i = 0; i < 256; ++i)
+        printf("U64_C(%" PRIu64 ")%s", r.U[i], (i == 255) ? "" : ((i + 1) % 4) ? ", " : ",\n");
+    printf("\n} };\n#endif /* LIMB_RABIN_TABLES_H */");
+
+    return 0;
+}