Imported Upstream version 0.36
authorAlexandre Mestiashvili <alex@biotec.tu-dresden.de>
Tue, 14 May 2013 11:40:51 +0000 (13:40 +0200)
committerAlexandre Mestiashvili <alex@biotec.tu-dresden.de>
Tue, 14 May 2013 11:40:51 +0000 (13:40 +0200)
META.yml
Makefile.PL
lib/Sereal/Encoder.pm
srl_encoder.c
srl_encoder.h
t/010_desperate.t
t/lib/Sereal/TestSet.pm

index 5b46ef7e4994bb3c340dd17dd7ad47af479d2f5f..208d57e1d6a0ead444fc07ac720c196facb464dd 100644 (file)
--- a/META.yml
+++ b/META.yml
@@ -1,6 +1,6 @@
 --- #YAML:1.0
 name:               Sereal-Encoder
-version:            0.35
+version:            0.36
 abstract:           Fast, compact, powerful binary serialization
 author:
     - Steffen Mueller <smueller@cpan.org>, Yves Orton <yves@cpan.org>
index ff2f5e7726d50064acfe718db1303504e35f05b5..2c5fd3325d442c102129f851c2b9556ce92931e3 100644 (file)
@@ -31,6 +31,8 @@ if ($Config{gccversion}) {
 
 if ($ENV{DEBUG}) {
   $OPTIMIZE .= ' -g';
+  $OPTIMIZE .= ' -Wextra' if $ENV{DEBUG} > 1 && $Config{gccversion};
+  $OPTIMIZE .= ' -pedantic' if $ENV{DEBUG} > 5 && $Config{gccversion}; # not pretty
 }
 else {
   $defines .= " -DNDEBUG";
index 54fd2f302b8d588d9bd776c036877460973bce15..fa75a93909b0082f320b19d6124589d1b81e5bfd 100644 (file)
@@ -5,7 +5,7 @@ use warnings;
 use Carp qw/croak/;
 use XSLoader;
 
-our $VERSION = '0.35'; # Don't forget to update the TestCompat set for testing against installed decoders!
+our $VERSION = '0.36'; # Don't forget to update the TestCompat set for testing against installed decoders!
 
 # not for public consumption, just for testing.
 my $TestCompat = [ map sprintf("%.2f", $_/100), reverse( 23 .. int($VERSION * 100) ) ]; # compat with 0.23 to ...
@@ -194,11 +194,12 @@ various caveats involved.
 
 =head3 dedupe_strings
 
-If true Sereal will use a hash to dedupe strings during serialization. This
-has a peformance and memory penalty so it defaults to off, but data structures
-with many duplicated strings will see a significant reduction in the size of
-the encoded form. Currently only strings longer than 3 characters will be
-deduped, however this may change in the future.
+If this is option is enabled/true then Sereal will use a hash to encode duplicates
+of strings during serialization efficiently using (internal) backreferences. This
+has a peformance and memory penalty during encoding so it defaults to off.
+On the other hand, data structures with many duplicated strings will see a
+significant reduction in the size of the encoded form. Currently only strings
+longer than 3 characters will be deduped, however this may change in the future.
 
 Note that Sereal will perform certain types of deduping automatically even
 without this option. In particular class names and hash keys are deduped
@@ -208,6 +209,28 @@ structure.
 
 Use of this option does not require an upgraded decoder. The deduping
 is performed in such a way that older decoders should handle it just fine.
+In other words, the output of a Sereal B<decoder> should not depend on
+whether this option was used during B<encoding>. See also below:
+I<aliased_dedupe_strings>.
+
+=head3 aliased_dedupe_strings
+
+This is an advanced option that should be used only after fully understanding
+its ramifications.
+
+This option enables a mode of operation that is similar to I<dedupe_strings>
+and if both options are set, I<aliased_dedupe_strings> takes precedence.
+
+The behaviour of I<aliased_dedupe_strings> differs from I<dedupe_strings>
+in that the duplicate occurrances of strings are emitted as Perl language
+level B<aliases> instead of as Sereal-internal backreferences. This means
+that using this option actually produces a different output data structure
+when decoding. The upshot is that with this option, the application
+using (decoding) the data may save a lot of memory in some situations
+but at the cost of potential action at a distance due to the aliasing.
+
+Beware: The test suite currently does not cover this option as well as it
+probably should. Patches welcome.
 
 =head1 INSTANCE METHODS
 
index 3ec6520756863b9a3c5ef4d91446e5f6bee19e16..914c3327eddcc901180d2522c9d4a6a6cca6b925 100644 (file)
@@ -244,57 +244,65 @@ srl_build_encoder_struct(pTHX_ HV *opt)
     /* load options */
     if (opt != NULL) {
         int undef_unknown = 0;
+        int snappy = 0;
         /* SRL_F_SHARED_HASHKEYS on by default */
         svp = hv_fetchs(opt, "no_shared_hashkeys", 0);
         if ( !svp || !SvTRUE(*svp) )
-            enc->flags |= SRL_F_SHARED_HASHKEYS;
+            SRL_ENC_SET_OPTION(enc, SRL_F_SHARED_HASHKEYS);
 
         svp = hv_fetchs(opt, "croak_on_bless", 0);
         if ( svp && SvTRUE(*svp) )
-            enc->flags |= SRL_F_CROAK_ON_BLESS;
+            SRL_ENC_SET_OPTION(enc, SRL_F_CROAK_ON_BLESS);
 
         svp = hv_fetchs(opt, "no_bless_objects", 0);
         if ( svp && SvTRUE(*svp) )
-            enc->flags |= SRL_F_NO_BLESS_OBJECTS;
+            SRL_ENC_SET_OPTION(enc, SRL_F_NO_BLESS_OBJECTS);
 
         svp = hv_fetchs(opt, "snappy", 0);
-        if ( svp && SvTRUE(*svp) )
-            enc->flags |= SRL_F_COMPRESS_SNAPPY;
+        if ( svp && SvTRUE(*svp) ) {
+            snappy = 1;
+            SRL_ENC_SET_OPTION(enc, SRL_F_COMPRESS_SNAPPY);
+        }
 
         svp = hv_fetchs(opt, "snappy_incr", 0);
-        if ( svp && SvTRUE(*svp) )
-            enc->flags |= SRL_F_COMPRESS_SNAPPY_INCREMENTAL;
+        if ( svp && SvTRUE(*svp) ) {
+            if (snappy)
+                croak("'snappy' and 'snappy_incr' options are mutually exclusive");
+            SRL_ENC_SET_OPTION(enc, SRL_F_COMPRESS_SNAPPY_INCREMENTAL);
+        }
 
         svp = hv_fetchs(opt, "undef_unknown", 0);
         if ( svp && SvTRUE(*svp) ) {
             undef_unknown = 1;
-            enc->flags |= SRL_F_UNDEF_UNKNOWN;
+            SRL_ENC_SET_OPTION(enc, SRL_F_UNDEF_UNKNOWN);
         }
 
         svp = hv_fetchs(opt, "sort_keys", 0);
-        if ( svp && SvTRUE(*svp) ) {
-            enc->flags |= SRL_F_SORT_KEYS;
-        }
+        if ( svp && SvTRUE(*svp) )
+            SRL_ENC_SET_OPTION(enc, SRL_F_SORT_KEYS);
 
-        svp = hv_fetchs(opt, "dedupe_strings", 0);
-        if ( svp && SvTRUE(*svp) ) {
-            enc->flags |= SRL_F_DEDUPE_STRINGS;
+        svp = hv_fetchs(opt, "aliased_dedupe_strings", 0);
+        if ( svp && SvTRUE(*svp) )
+            SRL_ENC_SET_OPTION(enc, SRL_F_ALIASED_DEDUPE_STRINGS | SRL_F_DEDUPE_STRINGS);
+        else {
+            svp = hv_fetchs(opt, "dedupe_strings", 0);
+            if ( svp && SvTRUE(*svp) )
+                SRL_ENC_SET_OPTION(enc, SRL_F_DEDUPE_STRINGS);
         }
 
         svp = hv_fetchs(opt, "stringify_unknown", 0);
         if ( svp && SvTRUE(*svp) ) {
-            if (expect_false( undef_unknown )) {
+            if (expect_false( undef_unknown ))
                 croak("'undef_unknown' and 'stringify_unknown' "
                       "options are mutually exclusive");
-            }
-            enc->flags |= SRL_F_STRINGIFY_UNKNOWN;
+            SRL_ENC_SET_OPTION(enc, SRL_F_STRINGIFY_UNKNOWN);
         }
 
         svp = hv_fetchs(opt, "warn_unknown", 0);
         if ( svp && SvTRUE(*svp) ) {
-            enc->flags |= SRL_F_WARN_UNKNOWN;
+            SRL_ENC_SET_OPTION(enc, SRL_F_WARN_UNKNOWN);
             if (SvIV(*svp) < 0)
-                enc->flags |= SRL_F_NOWARN_UNKNOWN_OVERLOAD;
+                SRL_ENC_SET_OPTION(enc, SRL_F_NOWARN_UNKNOWN_OVERLOAD);
         }
 
         svp = hv_fetchs(opt, "snappy_threshold", 0);
@@ -309,7 +317,7 @@ srl_build_encoder_struct(pTHX_ HV *opt)
     }
     else {
         /* SRL_F_SHARED_HASHKEYS on by default */
-        enc->flags |= SRL_F_SHARED_HASHKEYS;
+        SRL_ENC_SET_OPTION(enc, SRL_F_SHARED_HASHKEYS);
     }
 
     DEBUG_ASSERT_BUF_SANE(enc);
@@ -355,6 +363,20 @@ srl_init_string_deduper_hv(pTHX_ srl_encoder_t *enc)
     return enc->string_deduper_hv;
 }
 
+/* Lazy working buffer alloc */
+SRL_STATIC_INLINE void
+srl_init_snappy_workmem(pTHX_ srl_encoder_t *enc)
+{
+    /* Lazy working buffer alloc */
+    if (expect_false( enc->snappy_workmem == NULL )) {
+        /* Cleaned up automatically by the cleanup handler */
+        Newx(enc->snappy_workmem, CSNAPPY_WORKMEM_BYTES, char);
+        if (enc->snappy_workmem == NULL)
+            croak("Out of memory!");
+    }
+}
+
+
 void
 srl_write_header(pTHX_ srl_encoder_t *enc)
 {
@@ -506,11 +528,12 @@ srl_dump_classname(pTHX_ srl_encoder_t *enc, SV *src)
 }
 
 
-void
-srl_dump_data_structure(pTHX_ srl_encoder_t *enc, SV *src)
+/* Prepare encoder for encoding: Clone if already in use since
+ * encoders aren't "reentrant". Set as in use and register cleanup
+ * routine with Perl. */
+SRL_STATIC_INLINE srl_encoder_t *
+srl_prepare_encoder(pTHX_ srl_encoder_t *enc)
 {
-    if (DEBUGHACK) warn("== start dump");
-
     /* Check whether encoder is in use and create a new one on the
      * fly if necessary. Should only happen in bizarre edge cases... hopefully. */
     if (SRL_ENC_HAVE_OPER_FLAG(enc, SRL_OF_ENCODER_DIRTY)) {
@@ -524,6 +547,56 @@ srl_dump_data_structure(pTHX_ srl_encoder_t *enc, SV *src)
     /* Register our structure for destruction on scope exit */
     SAVEDESTRUCTOR_X(&srl_destructor_hook, (void *)enc);
 
+    return enc;
+}
+
+
+/* Update a varint anywhere in the output stream with defined start and end
+ * positions. This can produce non-canonical varints and is useful for filling
+ * pre-allocated varints. */
+SRL_STATIC_INLINE void
+srl_update_varint_from_to(pTHX_ char *varint_start, char *varint_end, UV number)
+{
+    while (number >= 0x80) {                      /* while we are larger than 7 bits long */
+        *varint_start++ = (number & 0x7f) | 0x80; /* write out the least significant 7 bits, set the high bit */
+        number = number >> 7;                     /* shift off the 7 least significant bits */
+    }
+    /* if it is the same size we can use a canonical varint */
+    if ( varint_start == varint_end ) {
+        *varint_start = number;                   /* encode the last 7 bits without the high bit being set */
+    } else {
+        /* if not we produce a non-canonical varint, basically we stuff
+         * 0 bits (via 0x80) into the "tail" of the varint, until we can
+         * stick in a null to terminate the sequence. This means that the
+         * varint is effectively "self-padding", and we only need special
+         * logic in the encoder - a decoder will happily process a non-canonical
+         * varint with no problem */
+        *varint_start++ = (number & 0x7f) | 0x80;
+        while ( varint_start < varint_end )
+            *varint_start++ = 0x80;
+        *varint_start= 0;
+    }
+}
+
+
+/* Resets the Snappy-compression header flag to OFF.
+ * Obviously requires that a Sereal header was already written to the
+ * encoder's output buffer. */
+SRL_STATIC_INLINE void
+srl_reset_snappy_header_flag(srl_encoder_t *enc)
+{
+    /* sizeof(const char *) includes a count of \0 */
+    char *flags_and_version_byte = enc->buf_start + sizeof(SRL_MAGIC_STRING) - 1;
+    /* disable snappy flag in header */
+    *flags_and_version_byte = SRL_PROTOCOL_ENCODING_RAW |
+                              (*flags_and_version_byte & SRL_PROTOCOL_VERSION_MASK);
+}
+
+void
+srl_dump_data_structure(pTHX_ srl_encoder_t *enc, SV *src)
+{
+    enc = srl_prepare_encoder(aTHX_ enc);
+
     if (!SRL_ENC_HAVE_OPTION(enc, (SRL_F_COMPRESS_SNAPPY | SRL_F_COMPRESS_SNAPPY_INCREMENTAL))) {
         srl_write_header(aTHX_ enc);
         srl_dump_sv(aTHX_ enc, src);
@@ -536,23 +609,19 @@ srl_dump_data_structure(pTHX_ srl_encoder_t *enc, SV *src)
         /* Alas, have to write entire packet first since the header length
          * will determine offsets. */
         srl_write_header(aTHX_ enc);
-        sereal_header_len = enc->pos - enc->buf_start;
+        sereal_header_len = BUF_POS_OFS(enc);
         srl_dump_sv(aTHX_ enc, src);
         srl_fixup_weakrefs(aTHX_ enc);
         assert(BUF_POS_OFS(enc) > sereal_header_len);
         uncompressed_body_length = BUF_POS_OFS(enc) - sereal_header_len;
 
-        /* Don't bother with snappy compression at all if we have less than $threshold bytes of payload */
         if (enc->snappy_threshold > 0
             && uncompressed_body_length < (STRLEN)enc->snappy_threshold)
         {
-            /* sizeof(const char *) includes a count of \0 */
-            char *flags_and_version_byte = enc->buf_start + sizeof(SRL_MAGIC_STRING) - 1;
-            /* disable snappy flag in header */
-            *flags_and_version_byte = SRL_PROTOCOL_ENCODING_RAW |
-                                      (*flags_and_version_byte & SRL_PROTOCOL_VERSION_MASK);
+            /* Don't bother with snappy compression at all if we have less than $threshold bytes of payload */
+            srl_reset_snappy_header_flag(enc);
         }
-        else {
+        else { /* do snappy compression of body */
             char *old_buf;
             char *varint_start= NULL;
             char *varint_end;
@@ -561,17 +630,11 @@ srl_dump_data_structure(pTHX_ srl_encoder_t *enc, SV *src)
             /* Get uncompressed payload and total packet output (after compression) lengths */
             dest_len = csnappy_max_compressed_length(uncompressed_body_length) + sereal_header_len + 1;
 
-            if ( SRL_ENC_HAVE_OPTION(enc, SRL_F_COMPRESS_SNAPPY_INCREMENTAL ) ) {
+            /* Will have to embed compressed packet length as varint if in incremental mode */
+            if ( SRL_ENC_HAVE_OPTION(enc, SRL_F_COMPRESS_SNAPPY_INCREMENTAL ) )
                 dest_len += SRL_MAX_VARINT_LENGTH;
-            }
 
-            /* Lazy working buffer alloc */
-            if (expect_false( enc->snappy_workmem == NULL )) {
-                /* Cleaned up automatically by the cleanup handler */
-                Newx(enc->snappy_workmem, CSNAPPY_WORKMEM_BYTES, char);
-                if (enc->snappy_workmem == NULL)
-                    croak("Out of memory!");
-            }
+            srl_init_snappy_workmem(aTHX_ enc);
 
             /* Back up old buffer and allocate new one with correct size */
             old_buf = enc->buf_start;
@@ -587,72 +650,32 @@ srl_dump_data_structure(pTHX_ srl_encoder_t *enc, SV *src)
             Copy(old_buf, enc->pos, sereal_header_len, char);
             enc->pos += sereal_header_len;
 
+            /* Embed compressed packet length */
             if ( SRL_ENC_HAVE_OPTION(enc, SRL_F_COMPRESS_SNAPPY_INCREMENTAL ) ) {
                 varint_start= enc->pos;
                 srl_buf_cat_varint_nocheck(aTHX_ enc, 0, dest_len);
                 varint_end= enc->pos - 1;
             }
 
-            /*
-             * fprintf(stderr, "'%u' %u %u\n", enc->pos - enc->buf_start, uncompressed_body_length, (uncompressed_body_length+sereal_header_len));
-             * fprintf(stdout, "%7s!%1s\n", old_buf, old_buf+6);
-             */
             csnappy_compress(old_buf+sereal_header_len, (uint32_t)uncompressed_body_length, enc->pos, &dest_len,
                              enc->snappy_workmem, CSNAPPY_WORKMEM_BYTES_POWER_OF_TWO);
+            assert(dest_len != 0);
 
-            if ( varint_start ) {
-                /* overwrite the max size varint with the real size of the compressed data */
-                UV n= dest_len;
-                while (n >= 0x80) {                      /* while we are larger than 7 bits long */
-                    *varint_start++ = (n & 0x7f) | 0x80; /* write out the least significant 7 bits, set the high bit */
-                    n = n >> 7;                          /* shift off the 7 least significant bits */
-                }
-                /* if it is the same size we can use a canonical varint */
-                if ( varint_start == varint_end ) {
-                    *varint_start = n;                     /* encode the last 7 bits without the high bit being set */
-                } else {
-                    /* if not we produce a non-canonical varint, basically we stuff
-                     * 0 bits (via 0x80) into the "tail" of the varint, until we can
-                     * stick in a null to terminate the sequence. This means that the
-                     * varint is effectively "self-padding", and we only need special
-                     * logic in the encoder - a decoder will happily process a non-canonical
-                     * varint with no problem */
-                    *varint_start++ = (n & 0x7f) | 0x80;
-                    while ( varint_start < varint_end )
-                        *varint_start++ = 0x80;
-                    *varint_start= 0;
-                }
-            }
+            /* overwrite the max size varint with the real size of the compressed data */
+            if (varint_start)
+                srl_update_varint_from_to(aTHX_ varint_start, varint_end, dest_len);
 
-            /* fprintf(stderr, "%u, %u %u %u\n", dest_len, enc->pos[0], enc->pos[1], enc->pos[2]); */
-            assert(dest_len != 0);
             Safefree(old_buf);
             enc->pos += dest_len;
             assert(enc->pos <= enc->buf_end);
 
-#if 0
-            if (expect_false( dest_len >= uncompressed_length )) {
-                /* FAIL. Swap old buffer back. Unset Snappy option */
-                char *compressed_buf = enc->buf_start;
-                char *flags_and_version_byte;
-                enc->buf_start = old_buf;
-                enc->pos = old_buf + sereal_header_len + uncompressed_length;
-                /* disable snappy flag in header */
-                flags_and_version_byte = enc->buf_start + sizeof(SRL_MAGIC_STRING) - 1;
-                flags_and_version_byte = SRL_PROTOCOL_ENCODING_RAW |
-                                      (flags_and_version_byte & SRL_PROTOCOL_VERSION_MASK);
-            }
-            else {
-                Safefree(old_buf);
-                enc->pos += dest_len;
-#endif
-        }
-    }
+            /* TODO If compression didn't help, swap back to old, uncompressed buffer */
+        } /* end of "actually do snappy compression" */
+    } /* end of "want snappy compression?" */
 
     /* NOT doing a
      *   SRL_ENC_RESET_OPER_FLAG(enc, SRL_OF_ENCODER_DIRTY);
      * here because we're relying on the SAVEDESTRUCTOR_X call. */
-    if (DEBUGHACK) warn("== end dump");
 }
 
 SRL_STATIC_INLINE void
@@ -1008,15 +1031,19 @@ srl_dump_svpv(pTHX_ srl_encoder_t *enc, SV *src)
         if (!dupe_offset_he) {
             croak("out of memory (hv_fetch_ent returned NULL)");
         } else {
+            const char out_tag= SRL_ENC_HAVE_OPTION(enc, SRL_F_ALIASED_DEDUPE_STRINGS)
+                                ? SRL_HDR_ALIAS
+                                : SRL_HDR_COPY;
             SV *ofs_sv= HeVAL(dupe_offset_he);
             if (SvIOK(ofs_sv)) {
-                /* emit copy */
-                srl_buf_cat_varint(aTHX_ enc, SRL_HDR_COPY, SvIV(ofs_sv));
+                /* emit copy or alias */
+                srl_buf_cat_varint(aTHX_ enc, out_tag, SvIV(ofs_sv));
                 return;
             } else if (SvUOK(ofs_sv)) {
-                srl_buf_cat_varint(aTHX_ enc, SRL_HDR_COPY, SvUV(ofs_sv));
+                srl_buf_cat_varint(aTHX_ enc, out_tag, SvUV(ofs_sv));
                 return;
             } else {
+                /* start tracking this string */
                 sv_setuv(ofs_sv, (UV)BUF_POS_OFS(enc));
             }
         }
index a1703d3151a2f6f500bec12a1d2116d424c33828..64db2bfa196bece7ade4242ef3d793fa348f6fc5 100644 (file)
@@ -82,11 +82,18 @@ void srl_dump_data_structure(pTHX_ srl_encoder_t *enc, SV *src);
  * if the unsupported item has string overloading. */
 #define SRL_F_SORT_KEYS                      0x00200UL
 
+/* If set, use a hash to emit COPY() tags for all duplicated strings
+ * (slow, but great compression) */
 #define SRL_F_DEDUPE_STRINGS                 0x00400UL
 
+/* Like SRL_F_DEDUPE_STRINGS but emits ALIAS() instead of COPY() for
+ * non-class-name, non-hash-key strings that are deduped. If set,
+ * supersedes SRL_F_DEDUPE_STRINGS. */
+#define SRL_F_ALIASED_DEDUPE_STRINGS           0x00800UL
+
 /* If set in flags, then we serialize objects without class information.
  * Corresponds to the 'no_bless_objects' flag found in the Decoder. */
-#define SRL_F_NO_BLESS_OBJECTS                0x00800UL
+#define SRL_F_NO_BLESS_OBJECTS                0x01000UL
 
 /* Set while the encoder is in active use / dirty */
 #define SRL_OF_ENCODER_DIRTY                 1UL
index ea3251c94eed38bb601c1bd160b39c899c47330f..984b91445f1d9da075df634953b416cedfb17537 100644 (file)
@@ -24,6 +24,7 @@ use Test::More;
 run_tests("plain");
 run_tests("no_shared_hk", {no_shared_hashkeys => 1});
 run_tests("dedupe_strings", {dedupe_strings => 1});
+run_tests("aliased_dedupe_strings", {aliased_dedupe_strings => 1});
 done_testing();
 
 sub run_tests {
index d8212ec6f44e5fae95a7c6d572e163babdfb96f3..ebd9017e55b144b0fcf14ef88981b6bc10fdcb37 100644 (file)
@@ -172,6 +172,50 @@ our @BasicTests = (
 
     [{}, hash(), "empty hash ref"],
     [{foo => "baaaaar"}, hash(short_string("foo"),short_string("baaaaar")), "simple hash ref"],
+    [
+      [qw(foooo foooo foooo)],
+      sub {
+          my $opt = shift;
+          if ($opt->{dedupe_strings} || $opt->{aliased_dedupe_strings}) {
+              my $d = array_head(3);
+              my $pos = length($Header) + length($d);
+              my $tag = $opt->{aliased_dedupe_strings} ? SRL_HDR_ALIAS : SRL_HDR_COPY;
+              $d .= short_string("foooo") . chr($tag) . varint($pos)
+                    . chr($tag) . varint($pos);
+              return $d;
+          }
+          else {
+              return array(short_string("foooo"),short_string("foooo"), short_string("foooo"));
+          }
+      },
+      "ary ref with repeated string"
+    ],
+    [
+      [{foooo => "barrr"}, {barrr => "foooo"}],
+      array(hash(short_string("foooo"), short_string("barrr")),
+            hash(short_string("barrr"), short_string("foooo"))),
+      "ary ref of hash refs without repeated strings"
+    ],
+    [
+      [{foooo => "foooo"}, {foooo2 => "foooo"}],
+      sub {
+          my $opt = shift;
+          if ($opt->{dedupe_strings} || $opt->{aliased_dedupe_strings}) {
+              my $tag = $opt->{aliased_dedupe_strings} ? SRL_HDR_ALIAS : SRL_HDR_COPY;
+              my $d = array_head(2) . hash_head(2) . short_string("foooo");
+              my $pos = length($Header) + length($d);
+              $d .= short_string("foooo") . hash_head(2)
+                    . short_string("foooo2")
+                    . chr($tag) . varint($pos);
+              return $d;
+          }
+          else {
+              return array(hash(short_string("foooo"), short_string("foooo")),
+                           hash(short_string("foooo2"), short_string("foooo"))),
+          }
+      },
+      "ary ref of hash refs with repeated strings"
+    ],
     [$scalar_ref_for_repeating, chr(SRL_HDR_REFN).chr(0b0000_1001), "scalar ref to constant"],
     [[$scalar_ref_for_repeating, $scalar_ref_for_repeating],
         do {
@@ -628,36 +672,37 @@ sub run_roundtrip_tests_internal {
                     }
                     next;
                 };
-              my $decoded= $dec->($encoded);
-              ok( defined($decoded) == defined($data), "$name ($ename, $mname, decoded definedness)")
-                  or next;
-              my $encoded2 = $enc->($decoded);
-              ok(defined $encoded2, "$name ($ename, $mname, encoded2 defined)")
-                  or next;
-              my $decoded2 = $dec->($encoded2);
-              ok(defined($decoded2) == defined($data), "$name ($ename, $mname, decoded2 defined)")
-                  or next;
-              is_deeply($decoded, $data, "$name ($ename, $mname, decoded vs data)")
-                  or do {
-                      if ($ENV{DEBUG_DUMP}) {
-                          Dump($decoded);
-                          Dump($data);
-                      }
-                  };
-              is_deeply($decoded2, $data, "$name ($ename, $mname, decoded2 vs data)")
-                  or do {
-                      if ($ENV{DEBUG_DUMP}) {
-                          Dump($decoded2);
-                          Dump($data);
-                      }
-                  };
-              is_deeply($decoded, $decoded2, "$name ($ename, $mname, decoded vs decoded2)")
-                  or do {
-                      if ($ENV{DEBUG_DUMP}) {
-                          Dump($decoded);
-                          Dump($decoded2);
-                      }
-                  };
+            my $decoded= $dec->($encoded);
+            ok( defined($decoded) == defined($data), "$name ($ename, $mname, decoded definedness)")
+              or next;
+            my $encoded2 = $enc->($decoded);
+            ok(defined $encoded2, "$name ($ename, $mname, encoded2 defined)")
+              or next;
+            my $decoded2 = $dec->($encoded2);
+            ok(defined($decoded2) == defined($data), "$name ($ename, $mname, decoded2 defined)")
+              or next;
+            is_deeply($decoded, $data, "$name ($ename, $mname, decoded vs data)")
+              or do {
+                  if ($ENV{DEBUG_DUMP}) {
+                      Dump($decoded);
+                      Dump($data);
+                  }
+              };
+            is_deeply($decoded2, $data, "$name ($ename, $mname, decoded2 vs data)")
+              or do {
+                  if ($ENV{DEBUG_DUMP}) {
+                      Dump($decoded2);
+                      Dump($data);
+                  }
+              };
+            is_deeply($decoded, $decoded2, "$name ($ename, $mname, decoded vs decoded2)")
+              or do {
+                  if ($ENV{DEBUG_DUMP}) {
+                      Dump($decoded);
+                      Dump($decoded2);
+                  }
+              };
+            
             if (0) {
                 # It isnt really safe to test this way right now. The exact output
                 # of two runs of Sereal is not guaranteed to be the same due to the effect of