Skip to contentSkip to conversation

Implement MTE tag compression for swapped pages

PATCH Superseded Alexander Potapenko 4 participants 33 messages Jul 20, 2023 – Sep 27, 2023

Subsystems (4)

v4 of 12 Superseded Latest is v12 →

lore
Share & tools
lore
#0 lore raw

v4 cover Implement MTE tag compression for swapped pages

Currently, when MTE pages are swapped out, the tags are kept in the memory, occupying 128 bytes per page. This is especially problematic for devices that use zram-backed in-memory swap, because tags stored uncompressed in the heap effectively reduce the available amount of swap memory.

The RLE-based EA0 algorithm suggested by Evgenii Stepanov and implemented in this patch series is able to efficiently compress 128-byte tag buffers, resulting in practical compression ratio between 2.5x and 20x. In most cases it is possible to store the compressed data in 63-bit Xarray values, resulting in no extra memory allocations.

Our measurements show that EA0 provides better compression than existing kernel compression algorithms (LZ4, LZO, LZ4HC, ZSTD) can offer, because EA0 specifically targets 128-byte buffers.

To implement compression/decompression, we also extend <linux/bitmap.h> with methods to set/get bit values at arbitrary places in the map.

We refactor arch/arm64/mm/mteswap.c to support both the compressed (CONFIG_ARM64_MTE_COMP) and non-compressed case. For the former, in addition to tag compression, we move tag allocation from kmalloc() to separate kmem caches, providing greater locality and relaxing the alignment requirements.

v4:
- fixed a bunch of comments by Andy Shevchenko and Yury Norov
- added Documentation/arch/arm64/mte-tag-compression.rst

v3:
- as suggested by Andy Shevchenko, use bitmap_get_value()/bitmap_set_value() written by Syed Nayyar Waris
- switched to unsigned long to reduce typecasts
- simplified the compression code

v2:
- as suggested by Yuri Norov, replace the poorly implemented struct bitq with <linux/bitmap.h>

Alexander Potapenko (4):
lib/test_bitmap: add tests for bitmap_{set,get}_value()
arm64: mte: implement CONFIG_ARM64_MTE_COMP
arm64: mte: add a test for MTE tags compression
arm64: mte: add compression support to mteswap.c

Syed Nayyar Waris (1):
lib/bitmap: add bitmap_{set,get}_value()

 Documentation/arch/arm64/index.rst            |   1 +
 .../arch/arm64/mte-tag-compression.rst        | 212 ++++++++++
 arch/arm64/Kconfig                            |  19 +
 arch/arm64/include/asm/mtecomp.h              |  13 +
 arch/arm64/mm/Makefile                        |   7 +
 arch/arm64/mm/mtecomp.c                       | 382 ++++++++++++++++++
 arch/arm64/mm/mteswap.c                       |  20 +-
 arch/arm64/mm/mteswap.h                       |  12 +
 arch/arm64/mm/mteswap_comp.c                  |  54 +++
 arch/arm64/mm/mteswap_nocomp.c                |  38 ++
 arch/arm64/mm/test_mtecomp.c                  | 217 ++++++++++
 include/linux/bitmap.h                        |  60 +++
 lib/test_bitmap.c                             |  81 ++++
 13 files changed, 1105 insertions(+), 11 deletions(-)
 create mode 100644 Documentation/arch/arm64/mte-tag-compression.rst
 create mode 100644 arch/arm64/include/asm/mtecomp.h
 create mode 100644 arch/arm64/mm/mtecomp.c
 create mode 100644 arch/arm64/mm/mteswap.h
 create mode 100644 arch/arm64/mm/mteswap_comp.c
 create mode 100644 arch/arm64/mm/mteswap_nocomp.c
 create mode 100644 arch/arm64/mm/test_mtecomp.c
Signature · 1 line
2.41.0.487.g6d72f3e995-goog
#1 lore raw

PATCH v4 1/5 lib/bitmap: add bitmap_{set,get}_value()

From: Syed Nayyar Waris <[email protected]>

The two new functions allow setting/getting values of length up to BITS_PER_LONG bits at arbitrary position in the bitmap.

The code was taken from "bitops: Introduce the for_each_set_clump macro" by Syed Nayyar Waris with a couple of minor changes:
- instead of using roundup(), which adds an unnecessary dependency on <linux/math.h>, we calculate space as BITS_PER_LONG-offset;
- indentation is reduced by not using else-clauses (suggested by checkpatch for bitmap_get_value())

---
v4:
- Address comments by Andy Shevchenko and Yury Norov:
- prevent passing values >= 64 to GENMASK()
- fix commit authorship
- change comments
- check for unlikely(nbits==0)
- drop unnecessary const declarations
- fix kernel-doc comments
- rename bitmap_{get,set}_value() to bitmap_{read,write}()

---
 include/linux/bitmap.h | 60 ++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 60 insertions(+)
include/linux/bitmap.h +60 -0
index 03644237e1efb..bc21c09a2e038 100644
@@ -76,7 +76,11 @@ struct device;
  *  bitmap_to_arr32(buf, src, nbits)            Copy nbits from buf to u32[] dst
  *  bitmap_to_arr64(buf, src, nbits)            Copy nbits from buf to u64[] dst
  *  bitmap_get_value8(map, start)               Get 8bit value from map at start
+ *  bitmap_read(map, start, nbits)              Read an nbits-sized value from
+ *                                              map at start
  *  bitmap_set_value8(map, value, start)        Set 8bit value to map at start
+ *  bitmap_write(map, value, start, nbits)      Write an nbits-sized value to
+ *                                              map at start
  *
  * Note, bitmap_zero() and bitmap_fill() operate over the region of
  * unsigned longs, that is, bits behind bitmap till the unsigned long
@@ -583,6 +587,33 @@ static inline unsigned long bitmap_get_value8(const unsigned long *map,
 	return (map[index] >> offset) & 0xFF;
 }
 
+/**
+ * bitmap_read - read a value of n-bits from the memory region
+ * @map: address to the bitmap memory region
+ * @start: bit offset of the n-bit value
+ * @nbits: size of value in bits, up to BITS_PER_LONG
+ *
+ * Returns: value of nbits located at the @start bit offset within the @map
+ * memory region.
+ */
+static inline unsigned long bitmap_read(const unsigned long *map,
+					unsigned long start,
+					unsigned long nbits)
+{
+	size_t index = BIT_WORD(start);
+	unsigned long offset = start % BITS_PER_LONG;
+	unsigned long space = BITS_PER_LONG - offset;
+	unsigned long value_low, value_high;
+
+	if (unlikely(!nbits))
+		return 0;
+	if (space >= nbits)
+		return (map[index] >> offset) & GENMASK(nbits - 1, 0);
+	value_low = map[index] & BITMAP_FIRST_WORD_MASK(start);
+	value_high = map[index + 1] & BITMAP_LAST_WORD_MASK(start + nbits);
+	return (value_low >> offset) | (value_high << space);
+}
+
 /**
  * bitmap_set_value8 - set an 8-bit value within a memory region
  * @map: address to the bitmap memory region
@@ -599,6 +630,35 @@ static inline void bitmap_set_value8(unsigned long *map, unsigned long value,
 	map[index] |= value << offset;
 }
 
+/**
+ * bitmap_write - write n-bit value within a memory region
+ * @map: address to the bitmap memory region
+ * @value: value of nbits
+ * @start: bit offset of the n-bit value
+ * @nbits: size of value in bits, up to BITS_PER_LONG
+ */
+static inline void bitmap_write(unsigned long *map,
+				unsigned long value,
+				unsigned long start, unsigned long nbits)
+{
+	size_t index = BIT_WORD(start);
+	unsigned long offset = start % BITS_PER_LONG;
+	unsigned long space = BITS_PER_LONG - offset;
+
+	if (unlikely(!nbits))
+		return;
+	value &= GENMASK(nbits - 1, 0);
+	if (space >= nbits) {
+		map[index] &= ~(GENMASK(nbits - 1, 0) << offset);
+		map[index] |= value << offset;
+		return;
+	}
+	map[index] &= ~BITMAP_FIRST_WORD_MASK(start);
+	map[index] |= value << offset;
+	map[index + 1] &= ~BITMAP_LAST_WORD_MASK(start + nbits);
+	map[index + 1] |= (value >> space);
+}
+
 #endif /* __ASSEMBLY__ */
 
 #endif /* __LINUX_BITMAP_H */
Signature · 1 line
2.41.0.487.g6d72f3e995-goog
maintainer
#2 lore raw

Re: [PATCH v4 1/5] lib/bitmap: add bitmap_{set,get}_value()

Quoted 8 lines · Alexander Potapenko
include/linux/bitmap.h +8 -0
+/**
+ * bitmap_write - write n-bit value within a memory region
+ * @map: address to the bitmap memory region
+ * @value: value of nbits
+ * @start: bit offset of the n-bit value
+ * @nbits: size of value in bits, up to BITS_PER_LONG
+ */
+static inline void bitmap_write(unsigned long *map,

include/linux/bitmap.h static inline void bitmap_set_value8(unsigned long *map, unsigned long value, · line 650

Quoted 10 lines
include/linux/bitmap.h +10 -0
+				unsigned long value,
+				unsigned long start, unsigned long nbits)
+{
+	size_t index = BIT_WORD(start);
+	unsigned long offset = start % BITS_PER_LONG;
+	unsigned long space = BITS_PER_LONG - offset;
+
+	if (unlikely(!nbits))
+		return;
+	value &= GENMASK(nbits - 1, 0);

Strictly speaking, a 'value' shouldn't contain set bits beyond nbits because otherwise it's an out-of-bonds type of error.

This is kind of gray zone to me, because it's a caller's responsibility to provide correct input. But on the other hand, it would be a great headache debugging corrupted bitmaps.

Now that we've got a single user of the bitmap_write, and even more, it's wrapped with a helper, I think it would be reasonable to trim a 'value' in the helper, if needed.

Anyways, the comment must warn about that loudly...

include/linux/bitmap.h static inline void bitmap_set_value8(unsigned long *map, unsigned long value, · line 652

Quoted 2 lines
include/linux/bitmap.h +2 -0
+	if (space >= nbits) {
+		map[index] &= ~(GENMASK(nbits - 1, 0) << offset);

'GENMASK(nbits - 1, 0) << offset' looks really silly.

Review

Quoted 1 line
(quoted diff) +1 -0
+		map[index] |= value << offset;

Here you commit 2 reads and 2 writes for a word instead of one.

include/linux/bitmap.h static inline void bitmap_set_value8(unsigned long *map, unsigned long value, · line 656

Quoted 3 lines
include/linux/bitmap.h +3 -0
+		return;
+	}
+	map[index] &= ~BITMAP_FIRST_WORD_MASK(start);

~FIRST_WORD is the same as LAST WORD. I tried to replace, and it saves ~25 bytes of .text on x86_64.

include/linux/bitmap.h static inline void bitmap_set_value8(unsigned long *map, unsigned long value, · line 660

Quoted 4 lines
include/linux/bitmap.h +4 -0
+	map[index] |= value << offset;
+	map[index + 1] &= ~BITMAP_LAST_WORD_MASK(start + nbits);
+	map[index + 1] |= (value >> space);
+}

With all that I think the implementation should look something like this:

        unsigned long w, mask;

        if (unlikely(nbits == 0))
                return 0;

        map += BIT_WORD(start);
        start %= BITS_PER_LONG;
        end = start + nbits - 1;

        w = *map & (end < BITS_PER_LONG ? ~GENMASK(end, start) : BITMAP_LAST_WORD_MASK(start));
        *map = w | (value << start);

        if (end < BITS_PER_LONG)
                return;

        w = *++map & BITMAP_FIRST_WORD_MASK(end);
        *map = w | (value >> BITS_PER_LONG * 2 - end);

It's not tested, except the /s/~FIRST_WORD/LAST_WORD/ part, but I expect it should be more efficient.

Alexander, can you please try the above and compare?

In previous iteration, I asked you to share disassembly listings for the functions. Can you please do that now?

Regarding the rest of the series:
- I still see Evgenii's name in mtecomp.c, and EA0 references;
- git-am throws warning about trailing line;
- checkpatch warns 7 times;

Can you fix all the above before sending the new version?

Have you tested generic part against BE32, BE64 and LE32 architectures; and arch part against BE64? If not, please do.

You're mentioning that the compression ratio is 2 to 20x. Can you share the absolute numbers? If it's 1k vs 2k, I think most people just don't care...

Can you share the code that you used to measure the compression ratio? Would it make sense to export the numbers via sysfs?

Thanks,
Yury

maintainer
#3 lore raw

Re: [PATCH v4 1/5] lib/bitmap: add bitmap_{set,get}_value()

Quoted 27 lines · Yury Norov
Quoted 8 lines · Alexander Potapenko
include/linux/bitmap.h +8 -0
+/**
+ * bitmap_write - write n-bit value within a memory region
+ * @map: address to the bitmap memory region
+ * @value: value of nbits
+ * @start: bit offset of the n-bit value
+ * @nbits: size of value in bits, up to BITS_PER_LONG
+ */
+static inline void bitmap_write(unsigned long *map,

include/linux/bitmap.h static inline void bitmap_set_value8(unsigned long *map, unsigned long value, · line 650

Quoted 10 lines
include/linux/bitmap.h +10 -0
+				unsigned long value,
+				unsigned long start, unsigned long nbits)
+{
+	size_t index = BIT_WORD(start);
+	unsigned long offset = start % BITS_PER_LONG;
+	unsigned long space = BITS_PER_LONG - offset;
+
+	if (unlikely(!nbits))
+		return;
+	value &= GENMASK(nbits - 1, 0);

Strictly speaking, a 'value' shouldn't contain set bits beyond nbits because otherwise it's an out-of-bonds type of error.

This is kind of gray zone to me, because it's a caller's responsibility to provide correct input. But on the other hand, it would be a great headache debugging corrupted bitmaps.

Quoted 5 lines

Now that we've got a single user of the bitmap_write, and even more, it's wrapped with a helper, I think it would be reasonable to trim a 'value' in the helper, if needed.

Anyways, the comment must warn about that loudly...

OK. I spent a night with that, and I'm still not sure. Pseudo code that implements it looks like this:

        for (bit = 0; bit < nbits; bit++)
                assign_bit(start + bit, bitmap, val & BIT(bit));

And it ignores trailing bits. So if we're optimizing this pattern, we'd ignore these bits just as well...

Either way, whatever we decide, let's stay clear with our intentions and mention explicitly that tail bits are either must be zero, or ignored.

Alexander, can you add the snippet above to the comments for the bitmap_write() and bitmap_read(), as well as in the test? Also, if we decide to clear tail of the input value, would BITMAP_LAST_WORD_MASK() generate better code than GENMASK(nbits - 1, 0) does?

Commets are very appreciated.

Thanks,
Yury

#4 lore raw

Re: [PATCH v4 1/5] lib/bitmap: add bitmap_{set,get}_value()

Quoted 8 lines

Either way, whatever we decide, let's stay clear with our intentions and mention explicitly that tail bits are either must be zero, or ignored.

Alexander, can you add the snippet above to the comments for the bitmap_write() and bitmap_read(), as well as in the test? Also, if we decide to clear tail of the input value, would BITMAP_LAST_WORD_MASK() generate better code than GENMASK(nbits - 1, 0) does?

Added the snippet above to bitmap_write(). I think however it is obvious that bitmap_read() reads only nbits?

Quoted 4 lines

Commets are very appreciated.

Thanks,
Yury

Signature · 10 lines
Alexander Potapenko
Software Engineer

Google Germany GmbH
Erika-Mann-Straße, 33
80636 München

Geschäftsführer: Paul Manicle, Liana Sebastian
Registergericht und -nummer: Hamburg, HRB 86891
Sitz der Gesellschaft: Hamburg
#5 lore raw

Re: [PATCH v4 1/5] lib/bitmap: add bitmap_{set,get}_value()

In reply to Yury Norov
Quoted 5 lines · Yury Norov

include/linux/bitmap.h static inline void bitmap_set_value8(unsigned long *map, unsigned long value, · line 652

Quoted 1 line · Alexander Potapenko
include/linux/bitmap.h +1 -0
+		map[index] &= ~(GENMASK(nbits - 1, 0) << offset);

'GENMASK(nbits - 1, 0) << offset' looks really silly.

But you followed the thread to get a clue why it's written in this form, right?

...

Quoted 2 lines

With all that I think the implementation should look something like this:

I would go this way if and only if the code generation on main architectures with both GCC and clang is better.

And maybe even some performance tests need to be provided.

...

Quoted 4 lines

Alexander, can you please try the above and compare?

In previous iteration, I asked you to share disassembly listings for the functions. Can you please do that now?

Exactly what we need before going with your suggestion(s).

Signature · 2 lines
With Best Regards,
Andy Shevchenko
maintainer
#6 lore raw

Re: [PATCH v4 1/5] lib/bitmap: add bitmap_{set,get}_value()

Quoted 8 lines · Andy Shevchenko
Quoted 5 lines · Yury Norov

include/linux/bitmap.h static inline void bitmap_set_value8(unsigned long *map, unsigned long value, · line 652

Quoted 1 line · Alexander Potapenko
include/linux/bitmap.h +1 -0
+		map[index] &= ~(GENMASK(nbits - 1, 0) << offset);

'GENMASK(nbits - 1, 0) << offset' looks really silly.

But you followed the thread to get a clue why it's written in this form, right?

Yes, I did. But I don't expect everyone looking at kernel code would spend time recovering discussions that explain why that happened. So, at least it would be fine to drop a comment.

Quoted 9 lines

...

Quoted 2 lines

With all that I think the implementation should look something like this:

I would go this way if and only if the code generation on main architectures with both GCC and clang is better.

And maybe even some performance tests need to be provided.

For the following implementation:

void my_bitmap_write(unsigned long *map, unsigned long value,
unsigned long start, unsigned long nbits)

  {
          unsigned long w, end;
  
          if (unlikely(nbits == 0))
                  return;
  
          value &= GENMASK(nbits - 1, 0);
  
          map += BIT_WORD(start);
          start %= BITS_PER_LONG;
          end = start + nbits - 1;
  
          w = *map & (end < BITS_PER_LONG ? ~GENMASK(end, start) : BITMAP_LAST_WORD_MASK(start));
          *map = w | (value << start);
  
          if (end < BITS_PER_LONG)
                  return;
  
          w = *++map & BITMAP_LAST_WORD_MASK(end + 1 - BITS_PER_LONG);
          *map = w | (value >> (BITS_PER_LONG - start));

}

This is the bloat-o-meter output:

$ scripts/bloat-o-meter lib/test_bitmap.o.orig lib/test_bitmap.o
add/remove: 8/0 grow/shrink: 1/0 up/down: 2851/0 (2851)
Function                                     old     new   delta
test_bitmap_init                            3846    5484   +1638
test_bitmap_write_perf                         -     401    +401
bitmap_write                                   -     271    +271
my_bitmap_write                                -     248    +248
bitmap_read                                    -     229    +229
__pfx_test_bitmap_write_perf                   -      16     +16
__pfx_my_bitmap_write                          -      16     +16
__pfx_bitmap_write                             -      16     +16
__pfx_bitmap_read                              -      16     +16
Total: Before=36964, After=39815, chg +7.71%

And this is the performance test:

        for (cnt = 0; cnt < 5; cnt++) {
                time = ktime_get();
                for (nbits = 1; nbits <= BITS_PER_LONG; nbits++) {
                        for (i = 0; i < 1000; i++) {
                                if (i + nbits > 1000)
                                        break;
                                bitmap_write(bmap, val, i, nbits);
                        }
                }
                time = ktime_get() - time;
                pr_err("bitmap_write:\t%llu\t", time);

                time = ktime_get();
                for (nbits = 1; nbits <= BITS_PER_LONG; nbits++) {
                        for (i = 0; i < 1000; i++) {
                                if (i + nbits > 1000)
                                        break;
                                my_bitmap_write(bmap, val, i, nbits);
                        }
                }
                time = ktime_get() - time;
                pr_cont("%llu\n", time);
        }

Which on x86_64/kvm with GCC gives:
Orig My

[    1.630731] test_bitmap: bitmap_write:	299092	252764
[    1.631584] test_bitmap: bitmap_write:	299522	252554
[    1.632429] test_bitmap: bitmap_write:	299171	258665
[    1.633280] test_bitmap: bitmap_write:	299241	252794
[    1.634133] test_bitmap: bitmap_write:	306716	252934

So, it's ~15% difference in performance and 8% in size.

I don't insist on my implementation, but I think, we'd experiment for more with code generation.

Thanks,
Yury

#7 lore raw

Re: [PATCH v4 1/5] lib/bitmap: add bitmap_{set,get}_value()

Quoted 3 lines · Yury Norov
Quoted 2 lines · Andy Shevchenko
Quoted 1 line · Yury Norov

On Thu, Jul 20, 2023 at 07:39:52PM +0200, Alexander Potapenko wrote:

...

Quoted 7 lines
Quoted 3 lines
Quoted 1 line

'GENMASK(nbits - 1, 0) << offset' looks really silly.

But you followed the thread to get a clue why it's written in this form, right?

Yes, I did. But I don't expect everyone looking at kernel code would spend time recovering discussions that explain why that happened. So, at least it would be fine to drop a comment.

See also below.

...

Quoted 1 line
          w = *map & (end < BITS_PER_LONG ? ~GENMASK(end, start) : BITMAP_LAST_WORD_MASK(start));

This GENMASK() may generate worse code as compiler uses more instructions instead of simple approach with the above..

...

Quoted 3 lines
bitmap_write                                   -     271    +271
my_bitmap_write                                -     248    +248
bitmap_read                                    -     229    +229

my_ -- means your proposal? Do you mean you have it better in size than original one?

Signature · 2 lines
With Best Regards,
Andy Shevchenko
#8 lore raw

Re: [PATCH v4 1/5] lib/bitmap: add bitmap_{set,get}_value()

In reply to Yury Norov
Quoted 15 lines · Yury Norov
Quoted 13 lines · Alexander Potapenko
include/linux/bitmap.h +13 -0
+/**
+ * bitmap_write - write n-bit value within a memory region
+ * @map: address to the bitmap memory region
+ * @value: value of nbits
+ * @start: bit offset of the n-bit value
+ * @nbits: size of value in bits, up to BITS_PER_LONG
+ */
+static inline void bitmap_write(unsigned long *map,
+                             unsigned long value,
+                             unsigned long start, unsigned long nbits)
+{
+     size_t index = BIT_WORD(start);
+     unsigned long offset = start % BITS_PER_LONG;
Quoted 8 lines

include/linux/bitmap.h static inline void bitmap_set_value8(unsigned long *map, unsigned long value, · line 650

Quoted 5 lines
include/linux/bitmap.h +5 -0
+     unsigned long space = BITS_PER_LONG - offset;
+
+     if (unlikely(!nbits))
+             return;
+     value &= GENMASK(nbits - 1, 0);

Strictly speaking, a 'value' shouldn't contain set bits beyond nbits because otherwise it's an out-of-bonds type of error.

I can easily imagine someone passing -1 (or ~0) as a value, but wanting to only write n bits of n.

Quoted 9 lines

This is kind of gray zone to me, because it's a caller's responsibility to provide correct input. But on the other hand, it would be a great headache debugging corrupted bitmaps.

Now that we've got a single user of the bitmap_write, and even more, it's wrapped with a helper, I think it would be reasonable to trim a 'value' in the helper, if needed.

Quoted 6 lines

Anyways, the comment must warn about that loudly...

include/linux/bitmap.h static inline void bitmap_set_value8(unsigned long *map, unsigned long value, · line 652

Quoted 2 lines
include/linux/bitmap.h +2 -0
+     if (space >= nbits) {
+             map[index] &= ~(GENMASK(nbits - 1, 0) << offset);

'GENMASK(nbits - 1, 0) << offset' looks really silly.

As noted in the other patch discussion, pulling offset into GENMASK is actually not an identity transformation, because nbits + offset may exceed 64, producing an invalid mask.

Quoted 4 lines
Quoted 2 lines

+ map[index] |= value << offset;

Here you commit 2 reads and 2 writes for a word instead of one.

There won't be two reads and two writes.
The compiler will read map[index] to a register, apply the mask, then write value << offset to it, then perform the write. Unless map[] is a volatile, repeated reads/writes will be optimized out by any decent compiler.

Quoted 34 lines

include/linux/bitmap.h static inline void bitmap_set_value8(unsigned long *map, unsigned long value, · line 656

Quoted 4 lines
include/linux/bitmap.h +3 -0
+             return;
+     }
+     map[index] &= ~BITMAP_FIRST_WORD_MASK(start);

~FIRST_WORD is the same as LAST WORD. I tried to replace, and it saves ~25 bytes of .text on x86_64.

include/linux/bitmap.h static inline void bitmap_set_value8(unsigned long *map, unsigned long value, · line 660

Quoted 4 lines
include/linux/bitmap.h +4 -0
+     map[index] |= value << offset;
+     map[index + 1] &= ~BITMAP_LAST_WORD_MASK(start + nbits);
+     map[index + 1] |= (value >> space);
+}

With all that I think the implementation should look something like this:

        unsigned long w, mask;

        if (unlikely(nbits == 0))
                return 0;

        map += BIT_WORD(start);
        start %= BITS_PER_LONG;
        end = start + nbits - 1;

        w = *map & (end < BITS_PER_LONG ? ~GENMASK(end, start) : BITMAP_LAST_WORD_MASK(start));
        *map = w | (value << start);

        if (end < BITS_PER_LONG)
                return;

        w = *++map & BITMAP_FIRST_WORD_MASK(end);
        *map = w | (value >> BITS_PER_LONG * 2 - end);
Quoted 4 lines

It's not tested, except the /s/~FIRST_WORD/LAST_WORD/ part, but I expect it should be more efficient.

Alexander, can you please try the above and compare?

I like the idea of sharing the first write between the branches, and it can be made even shorter:

===========================================================
void bitmap_write_new(unsigned long *map, unsigned long value,
unsigned long start, unsigned long nbits)

{
        unsigned long offset;
        unsigned long space;
        size_t index;
        bool fit;

        if (unlikely(!nbits))
                return;

        value &= GENMASK(nbits - 1, 0);
        offset = start % BITS_PER_LONG;
        space = BITS_PER_LONG - offset;
        index = BIT_WORD(start);
        fit = space >= nbits;

        map[index] &= (fit ? (~(GENMASK(nbits - 1, 0) << offset)) :
~BITMAP_FIRST_WORD_MASK(start));
        map[index] |= value << offset;
        if (fit)
                return;

        map[index + 1] &= ~BITMAP_LAST_WORD_MASK(start + nbits);
        map[index + 1] |= (value >> space);
}

===========================================================

According to Godbolt (https://godbolt.org/z/n5Te779bf), this function is 32 bytes shorter than yours under x86 Clang, and 8 bytes - under GCC (which on the other hand does a poor job optimizing both).

Overall, given that there's currently a single user of these functions, isn't it premature to optimize them without knowing anything about their performance?

Quoted 2 lines

In previous iteration, I asked you to share disassembly listings for the functions. Can you please do that now?

Will godbolt work for you (see above)?

Quoted 3 lines

Regarding the rest of the series:
- I still see Evgenii's name in mtecomp.c, and EA0 references;

Will fix, thanks!

Quoted 1 line

- git-am throws warning about trailing line;

Interesting, I generate the patches using git format-patch, I thought git am should be the inversion of it. I'll check.

Quoted 1 line

- checkpatch warns 7 times;

Indeed, there were warnings that I ignored (e.g. one of them was caused by me adding extern symbols to the test module, as requested during the review process). I think I can fix all of them.

Quoted 5 lines

Can you fix all the above before sending the new version?

Have you tested generic part against BE32, BE64 and LE32 architectures; and arch part against BE64? If not, please do.

BE64 works, I'll also need to check the 32-bit versions as well. Note that MTE is an ARM64 feature (yet we still need to ensure bitops.h works on 32 bits).

Quoted 4 lines

You're mentioning that the compression ratio is 2 to 20x. Can you share the absolute numbers? If it's 1k vs 2k, I think most people just don't care...

I'll provide the exact numbers with the next patch series. Last time I checked, the order of magnitude was tens of megabytes.

Quoted 2 lines

Can you share the code that you used to measure the compression ratio? Would it make sense to export the numbers via sysfs?

For out-of-line allocations the data can be derived from /proc/slabinfo, but we don't calculate inline allocations. Agreed, a debugfs interface won't hurt.

maintainer
#9 lore raw

Re: [PATCH v4 1/5] lib/bitmap: add bitmap_{set,get}_value()

Quoted 25 lines · Alexander Potapenko
Quoted 15 lines · Yury Norov
Quoted 13 lines · Alexander Potapenko
include/linux/bitmap.h +13 -0
+/**
+ * bitmap_write - write n-bit value within a memory region
+ * @map: address to the bitmap memory region
+ * @value: value of nbits
+ * @start: bit offset of the n-bit value
+ * @nbits: size of value in bits, up to BITS_PER_LONG
+ */
+static inline void bitmap_write(unsigned long *map,
+                             unsigned long value,
+                             unsigned long start, unsigned long nbits)
+{
+     size_t index = BIT_WORD(start);
+     unsigned long offset = start % BITS_PER_LONG;
Quoted 8 lines

include/linux/bitmap.h static inline void bitmap_set_value8(unsigned long *map, unsigned long value, · line 650

Quoted 5 lines
include/linux/bitmap.h +5 -0
+     unsigned long space = BITS_PER_LONG - offset;
+
+     if (unlikely(!nbits))
+             return;
+     value &= GENMASK(nbits - 1, 0);

Strictly speaking, a 'value' shouldn't contain set bits beyond nbits because otherwise it's an out-of-bonds type of error.

Quoted 2 lines

I can easily imagine someone passing -1 (or ~0) as a value, but wanting to only write n bits of n.

This is an abuse of new API because we've got a bitmap_set(). But whatever, let's keep that masking.

...

Quoted 6 lines

I like the idea of sharing the first write between the branches, and it can be made even shorter:

===========================================================
void bitmap_write_new(unsigned long *map, unsigned long value,
unsigned long start, unsigned long nbits)

Quoted 14 lines
{
        unsigned long offset;
        unsigned long space;
        size_t index;
        bool fit;

        if (unlikely(!nbits))
                return;

        value &= GENMASK(nbits - 1, 0);
        offset = start % BITS_PER_LONG;
        space = BITS_PER_LONG - offset;
        index = BIT_WORD(start);
        fit = space >= nbits;

space >= nbits <=>
BITS_PER_LONG - offset >= nbits <=>
offset + nbits <= BITS_PER_LONG

Quoted 1 line
        map[index] &= (fit ? (~(GENMASK(nbits - 1, 0) << offset)) :

So here GENMASK(nbits + offset - 1, offset) is at max:
GENMASK(BITS_PER_LONG - 1, offset). And it never overflows, which is my point. Does it make sense?

Quoted 1 line

~BITMAP_FIRST_WORD_MASK(start));

As I said, ~BITMAP_FIRST_WORD_MASK() is the same as BITMAP_LAST_WORD_MASK() and vise-versa.

Quoted 13 lines
        map[index] |= value << offset;
        if (fit)
                return;

        map[index + 1] &= ~BITMAP_LAST_WORD_MASK(start + nbits);
        map[index + 1] |= (value >> space);
}

===========================================================

According to Godbolt (https://godbolt.org/z/n5Te779bf), this function is 32 bytes shorter than yours under x86 Clang, and 8 bytes - under GCC (which on the other hand does a poor job optimizing both).

Quoted 8 lines

Overall, given that there's currently a single user of these functions, isn't it premature to optimize them without knowing anything about their performance?

Quoted 2 lines

In previous iteration, I asked you to share disassembly listings for the functions. Can you please do that now?

Will godbolt work for you (see above)?

I don't know for how long an external resource will keep the reference alive. My SSD keeps emails long enough.

...

Quoted 6 lines
Quoted 3 lines

You're mentioning that the compression ratio is 2 to 20x. Can you share the absolute numbers? If it's 1k vs 2k, I think most people just don't care...

I'll provide the exact numbers with the next patch series. Last time I checked, the order of magnitude was tens of megabytes.

That's impressive. Fruitful idea. It would be important for embedded guys who may disable MTE because of memory overhead. I think it's worth to mention that in Kconfig together with associate performance overhead, if it ever measurable.

Quoted 6 lines
Quoted 2 lines

Can you share the code that you used to measure the compression ratio? Would it make sense to export the numbers via sysfs?

For out-of-line allocations the data can be derived from /proc/slabinfo, but we don't calculate inline allocations. Agreed, a debugfs interface won't hurt.

#10 lore raw

Re: [PATCH v4 1/5] lib/bitmap: add bitmap_{set,get}_value()

Quoted 9 lines

space >= nbits <=>
BITS_PER_LONG - offset >= nbits <=>
offset + nbits <= BITS_PER_LONG

Quoted 1 line
        map[index] &= (fit ? (~(GENMASK(nbits - 1, 0) << offset)) :

So here GENMASK(nbits + offset - 1, offset) is at max:
GENMASK(BITS_PER_LONG - 1, offset). And it never overflows, which is my point. Does it make sense?

It indeed does. Perhaps pulling offset inside GENMASK is not a bug after all (a simple test does not show any difference between their behavior.
But GENMASK(nbits - 1 + offset, offset) blows up the code (see below). My guess is that this happens because the compiler fails to reuse the value of GENMASK(nbits - 1, 0) used to clamp the value to write, and calculates GENMASK(nbits - 1 + offset, offset) from scratch.

Quoted 5 lines
Quoted 2 lines

~BITMAP_FIRST_WORD_MASK(start));

As I said, ~BITMAP_FIRST_WORD_MASK() is the same as BITMAP_LAST_WORD_MASK() and vise-versa.

Surprisingly, ~BITMAP_FIRST_WORD_MASK() generates better code than BITMAP_LAST_WORD_MASK().

Quoted 6 lines
Quoted 6 lines
        map[index] |= value << offset;
        if (fit)
                return;

        map[index + 1] &= ~BITMAP_LAST_WORD_MASK(start + nbits);

OTOH I managed to shave three more bytes off by replacing ~BITMAP_LAST_WORD_MASK with a BITMAP_FIRST_WORD_MASK here.

Quoted 2 lines
Quoted 2 lines
        map[index + 1] |= (value >> space);
}

I'll post the implementations together with the disassembly below. I used some Clang 17.0.0 version that is a couple months behind upstream, but that still produces sustainably shorter code (~48 bytes less) than the trunk GCC on Godbolt.

1. Original implementation of bitmap_write() from this patch - 164 bytes (interestingly, it's 157 bytes with Clang 14.0.6)

==================================================================
void bitmap_write(unsigned long *map, unsigned long value,
unsigned long start, unsigned long nbits)

{
        size_t index = BIT_WORD(start);
        unsigned long offset = start % BITS_PER_LONG;
        unsigned long space = BITS_PER_LONG - offset;

        if (unlikely(!nbits))
                return;
        value &= GENMASK(nbits - 1, 0);
        if (space >= nbits) {
                map[index] &= ~(GENMASK(nbits - 1, 0) << offset);
                map[index] |= value << offset;
                return;
        }
        map[index] &= ~BITMAP_FIRST_WORD_MASK(start);
        map[index] |= value << offset;
        map[index + 1] &= ~BITMAP_LAST_WORD_MASK(start + nbits);
        map[index + 1] |= (value >> space);
}
==================================================================
0000000000001200 <bitmap_write>:
    1200: 41 57                push   %r15
    1202: 41 56                push   %r14
    1204: 53                    push   %rbx
    1205: 48 85 c9              test   %rcx,%rcx
    1208: 74 7b                je     1285 <bitmap_write+0x85>
    120a: 48 89 c8              mov    %rcx,%rax
    120d: 49 89 d2              mov    %rdx,%r10
    1210: 49 c1 ea 06          shr    $0x6,%r10
    1214: 41 89 d1              mov    %edx,%r9d
    1217: f6 d9                neg    %cl
    1219: 49 c7 c7 ff ff ff ff mov    $0xffffffffffffffff,%r15
    1220: 49 d3 ef              shr    %cl,%r15
    1223: 41 83 e1 3f          and    $0x3f,%r9d
    1227: 41 b8 40 00 00 00    mov    $0x40,%r8d
    122d: 4c 21 fe              and    %r15,%rsi
    1230: 48 89 f3              mov    %rsi,%rbx
    1233: 44 89 c9              mov    %r9d,%ecx
    1236: 48 d3 e3              shl    %cl,%rbx
    1239: 4d 29 c8              sub    %r9,%r8
    123c: 49 c7 c3 ff ff ff ff mov    $0xffffffffffffffff,%r11
    1243: 4e 8b 34 d7          mov    (%rdi,%r10,8),%r14
    1247: 49 39 c0              cmp    %rax,%r8
    124a: 73 3f                jae    128b <bitmap_write+0x8b>
    124c: 49 c7 c7 ff ff ff ff mov    $0xffffffffffffffff,%r15
    1253: 44 89 c9              mov    %r9d,%ecx
    1256: 49 d3 e7              shl    %cl,%r15
    1259: 49 f7 d7              not    %r15
    125c: 4d 21 fe              and    %r15,%r14
    125f: 49 09 de              or     %rbx,%r14
    1262: 4e 89 34 d7          mov    %r14,(%rdi,%r10,8)
    1266: 01 c2                add    %eax,%edx
    1268: f6 da                neg    %dl
    126a: 89 d1                mov    %edx,%ecx
    126c: 49 d3 eb              shr    %cl,%r11
    126f: 49 f7 d3              not    %r11
    1272: 44 89 c1              mov    %r8d,%ecx
    1275: 48 d3 ee              shr    %cl,%rsi
    1278: 4e 23 5c d7 08        and    0x8(%rdi,%r10,8),%r11
    127d: 4c 09 de              or     %r11,%rsi
    1280: 4a 89 74 d7 08        mov    %rsi,0x8(%rdi,%r10,8)
    1285: 5b                    pop    %rbx
    1286: 41 5e                pop    %r14
    1288: 41 5f                pop    %r15
    128a: c3                    ret
    128b: 44 89 c9              mov    %r9d,%ecx
    128e: 49 d3 e7              shl    %cl,%r15
    1291: 49 f7 d7              not    %r15
    1294: 4d 21 fe              and    %r15,%r14
    1297: 49 09 de              or     %rbx,%r14
    129a: 4e 89 34 d7          mov    %r14,(%rdi,%r10,8)
    129e: 5b                    pop    %rbx
    129f: 41 5e                pop    %r14
    12a1: 41 5f                pop    %r15
    12a3: c3                    ret

==================================================================

2. Your implementation, my_bitmap_write() - 152 bytes (used to be slightly worse with Clang 14.0.6 - 159 bytes):

==================================================================
void my_bitmap_write(unsigned long *map, unsigned long value,
unsigned long start, unsigned long nbits)

{
        unsigned long w, end;

        if (unlikely(nbits == 0))
                return;

        value &= GENMASK(nbits - 1, 0);

        map += BIT_WORD(start);
        start %= BITS_PER_LONG;
        end = start + nbits - 1;

        w = *map & (end < BITS_PER_LONG ? ~GENMASK(end, start) :
BITMAP_LAST_WORD_MASK(start));
        *map = w | (value << start);

        if (end < BITS_PER_LONG)
                return;

        w = *++map & BITMAP_LAST_WORD_MASK(end + 1 - BITS_PER_LONG);
        *map = w | (value >> (BITS_PER_LONG - start));
}
==================================================================
0000000000001160 <my_bitmap_write>:
    1160: 48 85 c9              test   %rcx,%rcx
    1163: 0f 84 8e 00 00 00    je     11f7 <my_bitmap_write+0x97>
    1169: 49 89 c9              mov    %rcx,%r9
    116c: f6 d9                neg    %cl
    116e: 48 d3 e6              shl    %cl,%rsi
    1171: 48 d3 ee              shr    %cl,%rsi
    1174: 49 89 d2              mov    %rdx,%r10
    1177: 49 c1 ea 06          shr    $0x6,%r10
    117b: 89 d0                mov    %edx,%eax
    117d: 83 e0 3f              and    $0x3f,%eax
    1180: 4e 8d 04 08          lea    (%rax,%r9,1),%r8
    1184: 4a 8d 0c 08          lea    (%rax,%r9,1),%rcx
    1188: 48 ff c9              dec    %rcx
    118b: 4e 8b 0c d7          mov    (%rdi,%r10,8),%r9
    118f: 48 83 f9 3f          cmp    $0x3f,%rcx
    1193: 77 29                ja     11be <my_bitmap_write+0x5e>
    1195: 41 f6 d8              neg    %r8b
    1198: 48 c7 c2 ff ff ff ff mov    $0xffffffffffffffff,%rdx
    119f: 44 89 c1              mov    %r8d,%ecx
    11a2: 48 d3 ea              shr    %cl,%rdx
    11a5: 89 c1                mov    %eax,%ecx
    11a7: 48 d3 ea              shr    %cl,%rdx
    11aa: 48 d3 e2              shl    %cl,%rdx
    11ad: 48 d3 e6              shl    %cl,%rsi
    11b0: 48 f7 d2              not    %rdx
    11b3: 49 21 d1              and    %rdx,%r9
    11b6: 4c 09 ce              or     %r9,%rsi
    11b9: 4a 89 34 d7          mov    %rsi,(%rdi,%r10,8)
    11bd: c3                    ret
    11be: f6 da                neg    %dl
    11c0: 89 d1                mov    %edx,%ecx
    11c2: 49 d3 e1              shl    %cl,%r9
    11c5: 49 d3 e9              shr    %cl,%r9
    11c8: 48 89 f2              mov    %rsi,%rdx
    11cb: 89 c1                mov    %eax,%ecx
    11cd: 48 d3 e2              shl    %cl,%rdx
    11d0: 4c 09 ca              or     %r9,%rdx
    11d3: 4a 89 14 d7          mov    %rdx,(%rdi,%r10,8)
    11d7: 4a 8b 54 d7 08        mov    0x8(%rdi,%r10,8),%rdx
    11dc: 41 f6 d8              neg    %r8b
    11df: 44 89 c1              mov    %r8d,%ecx
    11e2: 48 d3 e2              shl    %cl,%rdx
    11e5: 48 d3 ea              shr    %cl,%rdx
    11e8: f6 d8                neg    %al
    11ea: 89 c1                mov    %eax,%ecx
    11ec: 48 d3 ee              shr    %cl,%rsi
    11ef: 48 09 d6              or     %rdx,%rsi
    11f2: 4a 89 74 d7 08        mov    %rsi,0x8(%rdi,%r10,8)
    11f7: c3                    ret

==================================================================

3. My improved version built on top of yours and mentioned above under the name bitmap_write_new() - 116 bytes:

==================================================================
void bitmap_write_new(unsigned long *map, unsigned long value,
unsigned long start, unsigned long nbits)

{
        unsigned long offset;
        unsigned long space;
        size_t index;
        bool fit;

        if (unlikely(!nbits))
                return;

        value &= GENMASK(nbits - 1, 0);
        offset = start % BITS_PER_LONG;
        space = BITS_PER_LONG - offset;
        index = BIT_WORD(start);
        fit = space >= nbits;

        map[index] &= (fit ? (~(GENMASK(nbits - 1, 0) << offset)) :
~BITMAP_FIRST_WORD_MASK(start));
        map[index] |= value << offset;
        if (fit)
                return;

        map[index + 1] &= BITMAP_FIRST_WORD_MASK(start + nbits);
        map[index + 1] |= (value >> space);
}
==================================================================
00000000000012b0 <bitmap_write_new>:
    12b0: 48 85 c9              test   %rcx,%rcx
    12b3: 74 6e                je     1323 <bitmap_write_new+0x73>
    12b5: 48 89 c8              mov    %rcx,%rax
    12b8: f6 d9                neg    %cl
    12ba: 49 c7 c2 ff ff ff ff mov    $0xffffffffffffffff,%r10
    12c1: 49 d3 ea              shr    %cl,%r10
    12c4: 49 c7 c3 ff ff ff ff mov    $0xffffffffffffffff,%r11
    12cb: 4c 21 d6              and    %r10,%rsi
    12ce: 89 d1                mov    %edx,%ecx
    12d0: 83 e1 3f              and    $0x3f,%ecx
    12d3: 41 b8 40 00 00 00    mov    $0x40,%r8d
    12d9: 49 29 c8              sub    %rcx,%r8
    12dc: 49 89 d1              mov    %rdx,%r9
    12df: 49 c1 e9 06          shr    $0x6,%r9
    12e3: 49 39 c0              cmp    %rax,%r8
    12e6: 4d 0f 42 d3          cmovb  %r11,%r10
    12ea: 49 d3 e2              shl    %cl,%r10
    12ed: 49 f7 d2              not    %r10
    12f0: 4e 23 14 cf          and    (%rdi,%r9,8),%r10
    12f4: 49 89 f3              mov    %rsi,%r11
    12f7: 49 d3 e3              shl    %cl,%r11
    12fa: 4d 09 d3              or     %r10,%r11
    12fd: 4e 89 1c cf          mov    %r11,(%rdi,%r9,8)
    1301: 49 39 c0              cmp    %rax,%r8
    1304: 73 1d                jae    1323 <bitmap_write_new+0x73>
    1306: 01 d0                add    %edx,%eax
    1308: 4a 8b 54 cf 08        mov    0x8(%rdi,%r9,8),%rdx
    130d: 89 c1                mov    %eax,%ecx
    130f: 48 d3 ea              shr    %cl,%rdx
    1312: 48 d3 e2              shl    %cl,%rdx
    1315: 44 89 c1              mov    %r8d,%ecx
    1318: 48 d3 ee              shr    %cl,%rsi
    131b: 48 09 d6              or     %rdx,%rsi
    131e: 4a 89 74 cf 08        mov    %rsi,0x8(%rdi,%r9,8)
    1323: c3                    ret

==================================================================

4. The version of bitmap_write_new() that uses (GENMASK(nbits - 1 + offset, offset) - 146 bytes:

==================================================================
void bitmap_write_new_shift(unsigned long *map, unsigned long value,
unsigned long start, unsigned long nbits)

{
        unsigned long offset;
        unsigned long space;
        size_t index;
        bool fit;

        if (unlikely(!nbits))
                return;

        value &= GENMASK(nbits - 1, 0);
        offset = start % BITS_PER_LONG;
        space = BITS_PER_LONG - offset;
        index = BIT_WORD(start);
        fit = space >= nbits;

        map[index] &= (fit ? ~(GENMASK(nbits - 1 + offset, offset)) :
~BITMAP_FIRST_WORD_MASK(start));
        map[index] |= value << offset;
        if (fit)
                return;

        map[index + 1] &= BITMAP_FIRST_WORD_MASK(start + nbits);
        map[index + 1] |= (value >> space);
}
==================================================================
0000000000001330 <bitmap_write_new_shift>:
    1330: 48 85 c9              test   %rcx,%rcx
    1333: 74 6a                je     139f <bitmap_write_new_shift+0x6f>
    1335: 48 89 c8              mov    %rcx,%rax
    1338: f6 d9                neg    %cl
    133a: 48 d3 e6              shl    %cl,%rsi
    133d: 48 d3 ee              shr    %cl,%rsi
    1340: 41 89 d0              mov    %edx,%r8d
    1343: 41 83 e0 3f          and    $0x3f,%r8d
    1347: 41 b9 40 00 00 00    mov    $0x40,%r9d
    134d: 4d 29 c1              sub    %r8,%r9
    1350: 49 89 d2              mov    %rdx,%r10
    1353: 49 c1 ea 06          shr    $0x6,%r10
    1357: 49 c7 c3 ff ff ff ff mov    $0xffffffffffffffff,%r11
    135e: 44 89 c1              mov    %r8d,%ecx
    1361: 49 d3 e3              shl    %cl,%r11
    1364: 49 39 c1              cmp    %rax,%r9
    1367: 73 37                jae    13a0 <bitmap_write_new_shift+0x70>
    1369: 53                    push   %rbx
    136a: 49 f7 d3              not    %r11
    136d: 4e 23 1c d7          and    (%rdi,%r10,8),%r11
    1371: 48 89 f3              mov    %rsi,%rbx
    1374: 44 89 c1              mov    %r8d,%ecx
    1377: 48 d3 e3              shl    %cl,%rbx
    137a: 4c 09 db              or     %r11,%rbx
    137d: 4a 89 1c d7          mov    %rbx,(%rdi,%r10,8)
    1381: 01 d0                add    %edx,%eax
    1383: 4a 8b 54 d7 08        mov    0x8(%rdi,%r10,8),%rdx
    1388: 89 c1                mov    %eax,%ecx
    138a: 48 d3 ea              shr    %cl,%rdx
    138d: 48 d3 e2              shl    %cl,%rdx
    1390: 44 89 c9              mov    %r9d,%ecx
    1393: 48 d3 ee              shr    %cl,%rsi
    1396: 48 09 d6              or     %rdx,%rsi
    1399: 4a 89 74 d7 08        mov    %rsi,0x8(%rdi,%r10,8)
    139e: 5b                    pop    %rbx
    139f: c3                    ret
    13a0: 44 01 c0              add    %r8d,%eax
    13a3: f6 d8                neg    %al
    13a5: 89 c1                mov    %eax,%ecx
    13a7: 49 d3 e3              shl    %cl,%r11
    13aa: 49 d3 eb              shr    %cl,%r11
    13ad: 49 f7 d3              not    %r11
    13b0: 44 89 c1              mov    %r8d,%ecx
    13b3: 48 d3 e6              shl    %cl,%rsi
    13b6: 4e 23 1c d7          and    (%rdi,%r10,8),%r11
    13ba: 4c 09 de              or     %r11,%rsi
    13bd: 4a 89 34 d7          mov    %rsi,(%rdi,%r10,8)
    13c1: c3                    ret

==================================================================

For posterity, I am also attaching the C file containing the four implementations together with some code testing them.

PS. I won't be actively working on the patch series till the end of August, so feel free to ignore this letter until then.

#include <stddef.h>
#include <stdio.h>
#include <string.h>
#include <stdbool.h>

#define BITS_PER_LONG 64
#define unlikely(x) x
#define UL(x) (x##UL)

#define GENMASK(h, l) \
	(((~UL(0)) - (UL(1) << (l)) + 1) & \
	 (~UL(0) >> (BITS_PER_LONG - 1 - (h))))

#define BIT_WORD(nr) ((nr) / BITS_PER_LONG)
#define BITMAP_FIRST_WORD_MASK(start) (~0UL << ((start) & (BITS_PER_LONG - 1))) #define BITMAP_LAST_WORD_MASK(nbits) (~0UL >> (-(nbits) & (BITS_PER_LONG - 1)))

void my_bitmap_write(unsigned long *map, unsigned long value,
unsigned long start, unsigned long nbits)

{
	unsigned long w, end;

	if (unlikely(nbits == 0))
		return;

	value &= GENMASK(nbits - 1, 0);

	map += BIT_WORD(start);
	start %= BITS_PER_LONG;
	end = start + nbits - 1;

	w = *map & (end < BITS_PER_LONG ? ~GENMASK(end, start) : BITMAP_LAST_WORD_MASK(start));
	*map = w | (value << start);

	if (end < BITS_PER_LONG)
		return;

	w = *++map & BITMAP_LAST_WORD_MASK(end + 1 - BITS_PER_LONG);
	*map = w | (value >> (BITS_PER_LONG - start));
}

void bitmap_write(unsigned long *map, unsigned long value,
unsigned long start, unsigned long nbits)

{
	size_t index = BIT_WORD(start);
	unsigned long offset = start % BITS_PER_LONG;
	unsigned long space = BITS_PER_LONG - offset;

	if (unlikely(!nbits))
		return;
	value &= GENMASK(nbits - 1, 0);
	if (space >= nbits) {
		map[index] &= ~(GENMASK(nbits - 1, 0) << offset);
		map[index] |= value << offset;
		return;
	}
	map[index] &= ~BITMAP_FIRST_WORD_MASK(start);
	map[index] |= value << offset;
	map[index + 1] &= ~BITMAP_LAST_WORD_MASK(start + nbits);
	map[index + 1] |= (value >> space);
}

void bitmap_write_new(unsigned long *map, unsigned long value,
unsigned long start, unsigned long nbits)

{
	unsigned long offset;
	unsigned long space;
	size_t index;
	bool fit;

	if (unlikely(!nbits))
		return;

	value &= GENMASK(nbits - 1, 0);
	offset = start % BITS_PER_LONG;
	space = BITS_PER_LONG - offset;
	index =	BIT_WORD(start);
	fit = space >= nbits;

	map[index] &= (fit ? (~(GENMASK(nbits - 1, 0) << offset)) : ~BITMAP_FIRST_WORD_MASK(start));
	map[index] |= value << offset;
	if (fit)
		return;

	map[index + 1] &= BITMAP_FIRST_WORD_MASK(start + nbits);
	map[index + 1] |= (value >> space);
}

void bitmap_write_new_shift(unsigned long *map, unsigned long value,
unsigned long start, unsigned long nbits)

{
	unsigned long offset;
	unsigned long space;
	size_t index;
	bool fit;

	if (unlikely(!nbits))
		return;

	value &= GENMASK(nbits - 1, 0);
	offset = start % BITS_PER_LONG;
	space = BITS_PER_LONG - offset;
	index =	BIT_WORD(start);
	fit = space >= nbits;

	map[index] &= (fit ? ~(GENMASK(nbits - 1 + offset, offset)) : ~BITMAP_FIRST_WORD_MASK(start));
	map[index] |= value << offset;
	if (fit)
		return;

	map[index + 1] &= BITMAP_FIRST_WORD_MASK(start + nbits);
	map[index + 1] |= (value >> space);
}

#define MAPSIZE 3

void print_map(unsigned long *map, const char *c)

{
	int i;

	printf("%s: ", c);
	for (i = 0; i < MAPSIZE; i++) {
		printf("%lx ", map[i]);
	}
	printf("\n");
}

#define COMPARE(fn1, fn2) \

do {					\
	unsigned long one[MAPSIZE], two[MAPSIZE], three[MAPSIZE];\
	int res;						\
								\
	memset(one, 0, sizeof(unsigned long)*MAPSIZE);		\
	memset(two, 0, sizeof(unsigned long)*MAPSIZE);		\
	fn1(one, value, start, nbits);				\
	fn2(two, value, start, nbits);				\
	res = memcmp(one, two, sizeof(unsigned long)*MAPSIZE);	\
	if (res) {						\
		printf(#fn1 " vs. " #fn2 ": [%lu, %lu]: %d\n", 	\
			start, nbits, res);			\
		print_map(one, #fn1);				\
		print_map(two, #fn2);				\
	}							\
} while (0)

void test(unsigned long value, unsigned long start, unsigned long nbits)

{
	COMPARE(bitmap_write, bitmap_write_new);
	COMPARE(bitmap_write, my_bitmap_write);
	COMPARE(bitmap_write, bitmap_write_new_shift);
}

int main()

{
	unsigned long value = 0xfafafafafafafafaUL;
	unsigned long start, nbits;

	for (start = 0; start <= (MAPSIZE-1)*BITS_PER_LONG; start++) {
		for (nbits = 0; nbits <= BITS_PER_LONG; nbits++) {
			test(value, start, nbits);
		}
	}
	return 0;
}
maintainer
#11 lore raw

Re: [PATCH v4 1/5] lib/bitmap: add bitmap_{set,get}_value()

Quoted 10 lines · Alexander Potapenko
Quoted 9 lines

space >= nbits <=>
BITS_PER_LONG - offset >= nbits <=>
offset + nbits <= BITS_PER_LONG

Quoted 1 line
        map[index] &= (fit ? (~(GENMASK(nbits - 1, 0) << offset)) :

So here GENMASK(nbits + offset - 1, offset) is at max:
GENMASK(BITS_PER_LONG - 1, offset). And it never overflows, which is my point. Does it make sense?

Quoted 7 lines

It indeed does. Perhaps pulling offset inside GENMASK is not a bug after all (a simple test does not show any difference between their behavior.
But GENMASK(nbits - 1 + offset, offset) blows up the code (see below). My guess is that this happens because the compiler fails to reuse the value of GENMASK(nbits - 1, 0) used to clamp the value to write, and calculates GENMASK(nbits - 1 + offset, offset) from scratch.

OK. Can you put a comment explaining this? Or maybe would be even better to use BITMAP_LAST_WORD_MASK() here:

         mask = BITMAP_LAST_WORD_MASK(nbits);
         value &= mask;
         ...
         map[index] &= (fit ? (~mask << offset)) :
Quoted 7 lines
Quoted 4 lines
Quoted 1 line

~BITMAP_FIRST_WORD_MASK(start));

As I said, ~BITMAP_FIRST_WORD_MASK() is the same as BITMAP_LAST_WORD_MASK() and vise-versa.

Surprisingly, ~BITMAP_FIRST_WORD_MASK() generates better code than BITMAP_LAST_WORD_MASK().

Wow... If that's consistent across different compilers/arches, we'd just drop the latter. Thanks for pointing that. I'll check.

Quoted 12 lines
Quoted 5 lines
Quoted 5 lines
        map[index] |= value << offset;
        if (fit)
                return;

        map[index + 1] &= ~BITMAP_LAST_WORD_MASK(start + nbits);

OTOH I managed to shave three more bytes off by replacing ~BITMAP_LAST_WORD_MASK with a BITMAP_FIRST_WORD_MASK here.

Quoted 2 lines
Quoted 2 lines
        map[index + 1] |= (value >> space);
}
Quoted 7 lines

I'll post the implementations together with the disassembly below. I used some Clang 17.0.0 version that is a couple months behind upstream, but that still produces sustainably shorter code (~48 bytes less) than the trunk GCC on Godbolt.

1. Original implementation of bitmap_write() from this patch - 164 bytes (interestingly, it's 157 bytes with Clang 14.0.6)

I spotted that too in some other case. Newer compilers tend to generate bigger code, but the result usually works faster. One particular reason for my case was a loop unrolling.

[...]

Quoted 2 lines

3. My improved version built on top of yours and mentioned above under the name bitmap_write_new() - 116 bytes:

30% better in size - that's impressive!

Quoted 19 lines

==================================================================
void bitmap_write_new(unsigned long *map, unsigned long value,
unsigned long start, unsigned long nbits)

{
        unsigned long offset;
        unsigned long space;
        size_t index;
        bool fit;

        if (unlikely(!nbits))
                return;

        value &= GENMASK(nbits - 1, 0);
        offset = start % BITS_PER_LONG;
        space = BITS_PER_LONG - offset;
        index = BIT_WORD(start);
        fit = space >= nbits;

        map[index] &= (fit ? (~(GENMASK(nbits - 1, 0) << offset)) :
Quoted 8 lines
~BITMAP_FIRST_WORD_MASK(start));
        map[index] |= value << offset;
        if (fit)
                return;

        map[index + 1] &= BITMAP_FIRST_WORD_MASK(start + nbits);
        map[index + 1] |= (value >> space);
}

Thanks,
Yury

#12 lore raw

Re: [PATCH v4 1/5] lib/bitmap: add bitmap_{set,get}_value()

Quoted 4 lines · Yury Norov
Quoted 2 lines · Alexander Potapenko
Quoted 2 lines
Quoted 1 line

~BITMAP_FIRST_WORD_MASK(start));

Quoted 8 lines
Quoted 5 lines
Quoted 2 lines

As I said, ~BITMAP_FIRST_WORD_MASK() is the same as BITMAP_LAST_WORD_MASK() and vise-versa.

Surprisingly, ~BITMAP_FIRST_WORD_MASK() generates better code than BITMAP_LAST_WORD_MASK().

Wow... If that's consistent across different compilers/arches, we'd just drop the latter. Thanks for pointing that. I'll check.

It might be...

...

Quoted 2 lines
Quoted 2 lines
        map[index] &= (fit ? (~(GENMASK(nbits - 1, 0) << offset)) :

~BITMAP_FIRST_WORD_MASK(start));

...that both parts of ternary are using negation. So compiler may use negation only once. (Haven't looked at the assembly.)

Signature · 2 lines
With Best Regards,
Andy Shevchenko
#13 lore raw

Re: [PATCH v4 1/5] lib/bitmap: add bitmap_{set,get}_value()

In reply to Yury Norov
Quoted 7 lines

OK. Can you put a comment explaining this? Or maybe would be even better to use BITMAP_LAST_WORD_MASK() here:

         mask = BITMAP_LAST_WORD_MASK(nbits);
         value &= mask;
         ...
         map[index] &= (fit ? (~mask << offset)) :

I changed GENMASK to BITMAP_LAST_WORD_MASK here, and I think it's self-explanatory now, WDYT?

#14 lore raw

Re: [PATCH v4 1/5] lib/bitmap: add bitmap_{set,get}_value()

In reply to Yury Norov
Quoted 3 lines

Regarding the rest of the series:
- I still see Evgenii's name in mtecomp.c, and EA0 references;

Double-checked there are none in v5 (only the Suggested-by: tag)

Quoted 1 line

- git-am throws warning about trailing line;

I checked locally that git am does not warn about v5 patches. But given that the patches are generated with git format-patch I suspect they get garbled when you download them, could it be the case?

Quoted 1 line

- checkpatch warns 7 times;

It now warns 4 times, three warnings are about updating MAINTAINERS (I don't think there's need for this), the last one is about CONFIG_ARM64_MTE_COMP_KUNIT_TEST not having three lines of description text in Kconfig.

Quoted 4 lines

Can you fix all the above before sending the new version?

Have you tested generic part against BE32, BE64 and LE32 architectures; and arch part against BE64? If not, please do.

I did now.

Quoted 3 lines

You're mentioning that the compression ratio is 2 to 20x. Can you share the absolute numbers? If it's 1k vs 2k, I think most people just don't care...

In the other thread I mentioned that although 20x compression is reachable, it may not lead to practical savings. I reworded the description, having added the absolute numbers.

Quoted 2 lines

Can you share the code that you used to measure the compression ratio? Would it make sense to export the numbers via sysfs?

Done in v5

Quoted 2 lines

Thanks,
Yury

Signature · 10 lines
Alexander Potapenko
Software Engineer

Google Germany GmbH
Erika-Mann-Straße, 33
80636 München

Geschäftsführer: Paul Manicle, Liana Sebastian
Registergericht und -nummer: Hamburg, HRB 86891
Sitz der Gesellschaft: Hamburg
#15 lore raw

PATCH v4 2/5 lib/test_bitmap: add tests for bitmap_{set,get}_value()

In reply to Alexander Potapenko

Add basic tests ensuring that values can be added at arbitrary positions of the bitmap, including those spanning into the adjacent unsigned longs.

---
This patch was previously called
"lib/test_bitmap: add tests for bitmap_{set,get}_value_unaligned"
(https://lore.kernel.org/lkml/[email protected]/)

v4:
- Address comments by Andy Shevchenko: added Reviewed-by: and a link to the previous discussion
- Address comments by Yury Norov:
- expand the bitmap to catch more corner cases
- add code testing that bitmap_set_value() does not touch adjacent bits
- add code testing the nbits==0 case
- rename bitmap_{get,set}_value() to bitmap_{read,write}()

v3:
- switch to using bitmap_{set,get}_value()
- change the expected bit pattern in test_set_get_value(), as the test was incorrectly assuming 0 is the LSB.

---
 lib/test_bitmap.c | 81 +++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 81 insertions(+)
lib/test_bitmap.c +81 -0
index 187f5b2db4cf1..601000c7799df 100644
@@ -71,6 +71,17 @@ __check_eq_uint(const char *srcfile, unsigned int line,
 	return true;
 }
 
+static bool __init
+__check_eq_ulong(const char *srcfile, unsigned int line,
+		 const unsigned long exp_ulong, unsigned long x)
+{
+	if (exp_ulong != x) {
+		pr_err("[%s:%u] expected %lu, got %lu\n",
+			srcfile, line, exp_ulong, x);
+		return false;
+	}
+	return true;
+}
 
 static bool __init
 __check_eq_bitmap(const char *srcfile, unsigned int line,
@@ -186,6 +197,7 @@ __check_eq_str(const char *srcfile, unsigned int line,
 	})
 
 #define expect_eq_uint(...)		__expect_eq(uint, ##__VA_ARGS__)
+#define expect_eq_ulong(...)		__expect_eq(ulong, ##__VA_ARGS__)
 #define expect_eq_bitmap(...)		__expect_eq(bitmap, ##__VA_ARGS__)
 #define expect_eq_pbl(...)		__expect_eq(pbl, ##__VA_ARGS__)
 #define expect_eq_u32_array(...)	__expect_eq(u32_array, ##__VA_ARGS__)
@@ -1222,6 +1234,73 @@ static void __init test_bitmap_const_eval(void)
 	BUILD_BUG_ON(~var != ~BIT(25));
 }
 
+/*
+ * Test bitmap should be big enough to include the cases when start is not in
+ * the first word, and start+nbits lands in the following word.
+ */
+#define TEST_BIT_LEN (BITS_PER_LONG * 3)
+#define TEST_BYTE_LEN (BITS_TO_LONGS(TEST_BIT_LEN) * sizeof(unsigned long))
+static void __init test_set_get_value(void)
+{
+	DECLARE_BITMAP(bitmap, TEST_BIT_LEN);
+	DECLARE_BITMAP(exp_bitmap, TEST_BIT_LEN);
+	/* Prevent constant folding. */
+	volatile unsigned long zero_bits = 0;
+	unsigned long val, bit;
+	int i;
+
+	/* Setting/getting zero bytes should not crash the kernel. */
+	bitmap_write(NULL, 0, 0, zero_bits);
+	val = bitmap_read(NULL, 0, zero_bits);
+	expect_eq_ulong(0, val);
+
+	/*
+	 * Ensure that bitmap_read() reads the same value that was previously
+	 * written, and two consequent values are correctly merged.
+	 * The resulting bit pattern is asymmetric to rule out possible issues
+	 * with bit numeration order.
+	 */
+	for (i = 0; i < TEST_BIT_LEN - 7; i++) {
+		bitmap_zero(bitmap, TEST_BIT_LEN);
+		bitmap_write(bitmap, 0b10101UL, i, 5);
+		val = bitmap_read(bitmap, i, 5);
+		expect_eq_ulong(0b10101UL, val);
+		bitmap_write(bitmap, 0b101UL, i + 5, 3);
+		val = bitmap_read(bitmap, i + 5, 3);
+		expect_eq_ulong(0b101UL, val);
+		val = bitmap_read(bitmap, i, 8);
+		expect_eq_ulong(0b10110101UL, val);
+	}
+
+	/*
+	 * Check that setting a single bit does not accidentally touch the
+	 * adjacent bits.
+	 */
+	for (i = 0; i < TEST_BIT_LEN; i++) {
+		/*
+		 * A 0b10101010 pattern to catch both 0s replaced to 1s and vice
+		 * versa.
+		 */
+		memset(bitmap, 0xaa, TEST_BYTE_LEN);
+		memset(exp_bitmap, 0xaa, TEST_BYTE_LEN);
+		for (bit = 0; bit <= 1; bit++) {
+			bitmap_write(bitmap, bit, i, 1);
+			__assign_bit(i, exp_bitmap, bit);
+			expect_eq_bitmap(exp_bitmap, bitmap, TEST_BIT_LEN);
+		}
+	}
+
+	/* Ensure setting 0 bits does not change anything. */
+	memset(bitmap, 0xaa, TEST_BYTE_LEN);
+	memset(exp_bitmap, 0xaa, TEST_BYTE_LEN);
+	for (i = 0; i < TEST_BIT_LEN; i++) {
+		bitmap_write(bitmap, ~0UL, i, 0);
+		expect_eq_bitmap(exp_bitmap, bitmap, TEST_BIT_LEN);
+	}
+}
+#undef TEST_BYTE_LEN
+#undef TEST_BIT_LEN
+
 static void __init selftest(void)
 {
 	test_zero_clear();
@@ -1249,6 +1328,8 @@ static void __init selftest(void)
 	test_for_each_clear_bitrange_from();
 	test_for_each_set_clump8();
 	test_for_each_set_bit_wrap();
+
+	test_set_get_value();
 }
 
 KSTM_MODULE_LOADERS(test_bitmap);
Signature · 1 line
2.41.0.487.g6d72f3e995-goog
maintainer
#16 lore raw

Re: [PATCH v4 2/5] lib/test_bitmap: add tests for bitmap_{set,get}_value()

Quoted 6 lines · Alexander Potapenko

Add basic tests ensuring that values can be added at arbitrary positions of the bitmap, including those spanning into the adjacent unsigned longs.

[...]

lib/test_bitmap.c static void __init test_bitmap_const_eval(void) · line 1241

Quoted 5 lines
lib/test_bitmap.c +5 -0
+/*
+ * Test bitmap should be big enough to include the cases when start is not in
+ * the first word, and start+nbits lands in the following word.
+ */
+#define TEST_BIT_LEN (BITS_PER_LONG * 3)

Why not just 1000? Is your code safe against unaligned bitmaps?

Quoted 1 line

+#define TEST_BYTE_LEN (BITS_TO_LONGS(TEST_BIT_LEN) * sizeof(unsigned long))

BITS_TO_BYTES

Quoted 1 line

+static void __init test_set_get_value(void)

test_bitmap_read_write. Here, and in subjects for #1 and #2.

lib/test_bitmap.c static void __init test_bitmap_const_eval(void) · line 1248

Quoted 5 lines
lib/test_bitmap.c +5 -0
+{
+	DECLARE_BITMAP(bitmap, TEST_BIT_LEN);
+	DECLARE_BITMAP(exp_bitmap, TEST_BIT_LEN);
+	/* Prevent constant folding. */
+	volatile unsigned long zero_bits = 0;

Use READ_ONCE() instead of volatile

lib/test_bitmap.c static void __init test_bitmap_const_eval(void) · line 1255

Quoted 7 lines
lib/test_bitmap.c +7 -0
+	unsigned long val, bit;
+	int i;
+
+	/* Setting/getting zero bytes should not crash the kernel. */
+	bitmap_write(NULL, 0, 0, zero_bits);
+	val = bitmap_read(NULL, 0, zero_bits);
+	expect_eq_ulong(0, val);

No, val is undefined.

lib/test_bitmap.c static void __init test_bitmap_const_eval(void) · line 1263

Quoted 8 lines
lib/test_bitmap.c +8 -0
+
+	/*
+	 * Ensure that bitmap_read() reads the same value that was previously
+	 * written, and two consequent values are correctly merged.
+	 * The resulting bit pattern is asymmetric to rule out possible issues
+	 * with bit numeration order.
+	 */
+	for (i = 0; i < TEST_BIT_LEN - 7; i++) {

Can you add some empty lines in the block below in sake of readability? Maybe after expect()?

Quoted 17 lines
lib/test_bitmap.c +17 -0
+		bitmap_zero(bitmap, TEST_BIT_LEN);
+		bitmap_write(bitmap, 0b10101UL, i, 5);
+		val = bitmap_read(bitmap, i, 5);
+		expect_eq_ulong(0b10101UL, val);
+		bitmap_write(bitmap, 0b101UL, i + 5, 3);
+		val = bitmap_read(bitmap, i + 5, 3);
+		expect_eq_ulong(0b101UL, val);
+		val = bitmap_read(bitmap, i, 8);
+		expect_eq_ulong(0b10110101UL, val);
+	}
+
+	/*
+	 * Check that setting a single bit does not accidentally touch the
+	 * adjacent bits.
+	 */
+	for (i = 0; i < TEST_BIT_LEN; i++) {
+		/*

lib/test_bitmap.c static void __init test_bitmap_const_eval(void) · line 1290

Quoted 10 lines
lib/test_bitmap.c +10 -0
+		 * A 0b10101010 pattern to catch both 0s replaced to 1s and vice
+		 * versa.
+		 */
+		memset(bitmap, 0xaa, TEST_BYTE_LEN);
+		memset(exp_bitmap, 0xaa, TEST_BYTE_LEN);
+		for (bit = 0; bit <= 1; bit++) {
+			bitmap_write(bitmap, bit, i, 1);
+			__assign_bit(i, exp_bitmap, bit);
+			expect_eq_bitmap(exp_bitmap, bitmap, TEST_BIT_LEN);
+		}

I suggested the other test:

val = DEADBEEF;
for (nbits = 1; nbits <= BITS_PER_LONG; nbits++)

        for (start = 0; start < 1000; i++) {
                if (start + nbits >= 1000)
                        break;;

                v = val & GENMASK(nbits - 1, 0);

		memset(bitmap, 0xaa /* also 0xff and 0x00 */, TEST_BYTE_LEN);
		memset(exp_bitmap, 0xaa, TEST_BYTE_LEN);

                for (n = 0; n < nbits; n++)
			__assign_bit(v & BIT(n), exp_bitmap, start + n);

                bitmap_write(bitmap, v, start, nbits);
		expect_eq_bitmap(exp_bitmap, bitmap, 1000);

                r = bitmap_read(bitmap, start, nbits);
                expect_eq(r, v);
        }
Quoted 17 lines
lib/test_bitmap.c +13 -0
+	}
+
+	/* Ensure setting 0 bits does not change anything. */
+	memset(bitmap, 0xaa, TEST_BYTE_LEN);
+	memset(exp_bitmap, 0xaa, TEST_BYTE_LEN);
+	for (i = 0; i < TEST_BIT_LEN; i++) {
+		bitmap_write(bitmap, ~0UL, i, 0);
+		expect_eq_bitmap(exp_bitmap, bitmap, TEST_BIT_LEN);
+	}
+}
+#undef TEST_BYTE_LEN
+#undef TEST_BIT_LEN
+
 static void __init selftest(void)
 {
 	test_zero_clear();

lib/test_bitmap.c static void __init selftest(void) · line 1332

Quoted 5 lines
lib/test_bitmap.c +2 -0
@@ -1249,6 +1328,8 @@ static void __init selftest(void)
 	test_for_each_clear_bitrange_from();
 	test_for_each_set_clump8();
 	test_for_each_set_bit_wrap();
+
+	test_set_get_value();

This should append the test_bitmap_* section

Thanks,
Yury

#17 lore raw

Re: [PATCH v4 2/5] lib/test_bitmap: add tests for bitmap_{set,get}_value()

Quoted 7 lines

lib/test_bitmap.c static void __init test_bitmap_const_eval(void) · line 1241

Quoted 5 lines
lib/test_bitmap.c +5 -0
+/*
+ * Test bitmap should be big enough to include the cases when start is not in
+ * the first word, and start+nbits lands in the following word.
+ */
+#define TEST_BIT_LEN (BITS_PER_LONG * 3)

Why not just 1000? Is your code safe against unaligned bitmaps?

done

Quoted 3 lines
Quoted 1 line

+#define TEST_BYTE_LEN (BITS_TO_LONGS(TEST_BIT_LEN) * sizeof(unsigned long))

BITS_TO_BYTES

done

Quoted 4 lines
Quoted 2 lines

+static void __init test_set_get_value(void)

test_bitmap_read_write. Here, and in subjects for #1 and #2.

done

Quoted 7 lines

lib/test_bitmap.c static void __init test_bitmap_const_eval(void) · line 1248

Quoted 5 lines
lib/test_bitmap.c +5 -0
+{
+     DECLARE_BITMAP(bitmap, TEST_BIT_LEN);
+     DECLARE_BITMAP(exp_bitmap, TEST_BIT_LEN);
+     /* Prevent constant folding. */
+     volatile unsigned long zero_bits = 0;

Use READ_ONCE() instead of volatile

done

Quoted 9 lines

lib/test_bitmap.c static void __init test_bitmap_const_eval(void) · line 1255

Quoted 7 lines
lib/test_bitmap.c +7 -0
+     unsigned long val, bit;
+     int i;
+
+     /* Setting/getting zero bytes should not crash the kernel. */
+     bitmap_write(NULL, 0, 0, zero_bits);
+     val = bitmap_read(NULL, 0, zero_bits);
+     expect_eq_ulong(0, val);

No, val is undefined.

Why? bitmap_read(..., ..., 0) always returns 0.

Quoted 10 lines

lib/test_bitmap.c static void __init test_bitmap_const_eval(void) · line 1263

Quoted 9 lines
lib/test_bitmap.c +8 -0
+
+     /*
+      * Ensure that bitmap_read() reads the same value that was previously
+      * written, and two consequent values are correctly merged.
+      * The resulting bit pattern is asymmetric to rule out possible issues
+      * with bit numeration order.
+      */
+     for (i = 0; i < TEST_BIT_LEN - 7; i++) {
Quoted 2 lines

Can you add some empty lines in the block below in sake of readability? Maybe after expect()?

Done

(snip)

Quoted 14 lines

I suggested the other test:

val = DEADBEEF;
for (nbits = 1; nbits <= BITS_PER_LONG; nbits++)

        for (start = 0; start < 1000; i++) {
                if (start + nbits >= 1000)
                        break;;

                v = val & GENMASK(nbits - 1, 0);

                memset(bitmap, 0xaa /* also 0xff and 0x00 */, TEST_BYTE_LEN);
                memset(exp_bitmap, 0xaa, TEST_BYTE_LEN);

                for (n = 0; n < nbits; n++)
Quoted 8 lines
                        __assign_bit(v & BIT(n), exp_bitmap, start + n);

                bitmap_write(bitmap, v, start, nbits);
                expect_eq_bitmap(exp_bitmap, bitmap, 1000);

                r = bitmap_read(bitmap, start, nbits);
                expect_eq(r, v);
        }

I factored out test cases that can benefit from different background patterns into a separate function and added yours there. Thanks!

Quoted 8 lines

lib/test_bitmap.c static void __init selftest(void) · line 1332

Quoted 6 lines
lib/test_bitmap.c +2 -0
@@ -1249,6 +1328,8 @@ static void __init selftest(void)
      test_for_each_clear_bitrange_from();
      test_for_each_set_clump8();
      test_for_each_set_bit_wrap();
+
+     test_set_get_value();

This should append the test_bitmap_* section

Done

Quoted 2 lines

Thanks,
Yury

Thanks a lot for your comments!

maintainer
#18 lore raw

Re: [PATCH v4 2/5] lib/test_bitmap: add tests for bitmap_{set,get}_value()

Quoted 11 lines · Alexander Potapenko
Quoted 9 lines

lib/test_bitmap.c static void __init test_bitmap_const_eval(void) · line 1255

Quoted 7 lines
lib/test_bitmap.c +7 -0
+     unsigned long val, bit;
+     int i;
+
+     /* Setting/getting zero bytes should not crash the kernel. */
+     bitmap_write(NULL, 0, 0, zero_bits);
+     val = bitmap_read(NULL, 0, zero_bits);
+     expect_eq_ulong(0, val);

No, val is undefined.

Why? bitmap_read(..., ..., 0) always returns 0.

Because it's unexpected and most likely wrong to pass 0 bits. We guarantee that bitmap_read() will return immediately, and will not touch the memory. But we don't guarantee that we return any specific value.

It's not a hot path, at least now, and we can spend few extra cycles to clear output register and return 0, but user should not rely on it in any way, especially in a test that is intended to show an example of using the new API.

Consider a less relaxed environment, where we really have to count cycles. In such environment, we'd return a content of the 1st input argument, just because it's already in R0, and compiled doesn't have to:

    mov r0, #0
    ret
#19 lore raw

Re: [PATCH v4 2/5] lib/test_bitmap: add tests for bitmap_{set,get}_value()

Quoted 10 lines · Yury Norov
Quoted 8 lines · Alexander Potapenko
Quoted 8 lines
Quoted 7 lines
lib/test_bitmap.c +7 -0
+     unsigned long val, bit;
+     int i;
+
+     /* Setting/getting zero bytes should not crash the kernel. */
+     bitmap_write(NULL, 0, 0, zero_bits);
+     val = bitmap_read(NULL, 0, zero_bits);
+     expect_eq_ulong(0, val);
Quoted 8 lines
Quoted 3 lines
Quoted 1 line

No, val is undefined.

Why? bitmap_read(..., ..., 0) always returns 0.

Because it's unexpected and most likely wrong to pass 0 bits. We guarantee that bitmap_read() will return immediately, and will not touch the memory. But we don't guarantee that we return any specific value.

Fair enough, I'll remove the expect_eq_ulong() from the test in v6 and will also add the requirement for nbits to be nonzero to the doc comments for bitmap_read() and bitmap_write().

Quoted 4 lines

It's not a hot path, at least now, and we can spend few extra cycles to clear output register and return 0, but user should not rely on it in any way, especially in a test that is intended to show an example of using the new API.

Ok, for now I'll keep the "return 0" part.

Quoted 7 lines

Consider a less relaxed environment, where we really have to count cycles. In such environment, we'd return a content of the 1st input argument, just because it's already in R0, and compiled doesn't have to:

    mov r0, #0
    ret
#20 lore raw

PATCH v4 3/5 arm64: mte: implement CONFIG_ARM64_MTE_COMP

In reply to Alexander Potapenko

The config implements the algorithm compressing memory tags for ARM MTE during swapping.

The algorithm is based on RLE and specifically targets 128-byte buffers of tags corresponding to a single page. In the common case a buffer can be compressed into 63 bits, making it possible to store it without additional memory allocation.

---
v4:
- Addressed comments by Andy Shevchenko:
- expanded "MTE" to "Memory Tagging Extension" in Kconfig
- fixed kernel-doc comments, moved them to C source
- changed variables to unsigned where applicable
- some code simplifications, fewer unnecessary assignments
- added the mte_largest_idx_bits() helper
- added namespace prefixes to all functions
- added missing headers (but removed bits.h)
- Addressed comments by Yury Norov:
- removed test-only functions from mtecomp.h
- dropped the algoritm name (all functions are now prefixed with "mte")
- added more comments
- got rid of MTE_RANGES_INLINE
- renamed bitmap_{get,set}_value() to bitmap_{read,write}()
- moved the big comment explaining the algorithm to Documentation/arch/arm64/mte-tag-compression.rst, expanded it, add a link to it from Documentation/arch/arm64/index.rst
- removed hardcoded ranges from mte_alloc_size()/mte_size_to_ranges()

v3:
- Addressed comments by Andy Shevchenko:
- use bitmap_{set,get}_value() writte by Syed Nayyar Waris
- switched to unsigned long everywhere (fewer casts)
- simplified the code, removed redundant checks
- dropped ea0_compress_inline()
- added bit size constants and helpers to access the bitmap
- explicitly initialize all compressed sizes in ea0_compress_to_buf()
- initialize all handle bits

v2:
- as suggested by Yury Norov, switched from struct bitq (which is not needed anymore) to <linux/bitmap.h>
- add missing symbol exports

---
 Documentation/arch/arm64/index.rst            |   1 +
 .../arch/arm64/mte-tag-compression.rst        | 212 ++++++++++
 arch/arm64/Kconfig                            |   9 +
 arch/arm64/include/asm/mtecomp.h              |  13 +
 arch/arm64/mm/Makefile                        |   1 +
 arch/arm64/mm/mtecomp.c                       | 382 ++++++++++++++++++
 6 files changed, 618 insertions(+)
 create mode 100644 Documentation/arch/arm64/mte-tag-compression.rst
 create mode 100644 arch/arm64/include/asm/mtecomp.h
 create mode 100644 arch/arm64/mm/mtecomp.c
Documentation/arch/arm64/index.rst +1 -0
index d08e924204bf1..bf6c1583233a9 100644
@@ -19,6 +19,7 @@ ARM64 Architecture
     legacy_instructions
     memory
     memory-tagging-extension
+    mte-tag-compression
     perf
     pointer-authentication
     ptdump
Documentation/arch/arm64/mte-tag-compression.rst new file +212 -0

212 lines not on this page · raw message

arch/arm64/Kconfig +9 -0
index a2511b30d0f67..3ac6e302b1509 100644
@@ -2093,6 +2093,15 @@ config ARM64_EPAN
 	  if the cpu does not implement the feature.
 endmenu # "ARMv8.7 architectural features"
 
+config ARM64_MTE_COMP
+	bool "Tag compression for ARM64 Memory Tagging Extension"
+	default y
+	depends on ARM64_MTE
+	help
+	  Enable tag compression support for ARM64 Memory Tagging Extension.
+
+	  128-byte tag buffers corresponding to 4K pages can be compressed to save heap memory.
+
 config ARM64_SVE
 	bool "ARM Scalable Vector Extension support"
 	default y
arch/arm64/include/asm/mtecomp.h new file +13 -0
new file mode 100644
index 0000000000000..71552bc429882
@@ -0,0 +1,13 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+
+#ifndef __ASM_MTECOMP_H
+#define __ASM_MTECOMP_H
+
+#include <linux/types.h>
+
+unsigned long mte_compress(u8 *tags);
+bool mte_decompress(unsigned long handle, u8 *tags);
+void mte_release_handle(unsigned long handle);
+size_t mte_storage_size(unsigned long handle);
+
+#endif // __ASM_MTECOMP_H
arch/arm64/mm/Makefile +1 -0
index dbd1bc95967d0..46778f6dd83c2 100644
@@ -10,6 +10,7 @@ obj-$(CONFIG_TRANS_TABLE)	+= trans_pgd.o
 obj-$(CONFIG_TRANS_TABLE)	+= trans_pgd-asm.o
 obj-$(CONFIG_DEBUG_VIRTUAL)	+= physaddr.o
 obj-$(CONFIG_ARM64_MTE)		+= mteswap.o
+obj-$(CONFIG_ARM64_MTE_COMP)	+= mtecomp.o
 KASAN_SANITIZE_physaddr.o	+= n
 
 obj-$(CONFIG_KASAN)		+= kasan_init.o
arch/arm64/mm/mtecomp.c new file +382 -0

382 lines not on this page · raw message

Signature · 1 line
2.41.0.487.g6d72f3e995-goog
#21 lore raw

Re: [PATCH v4 3/5] arm64: mte: implement CONFIG_ARM64_MTE_COMP

Quoted 7 lines · Alexander Potapenko

The config implements the algorithm compressing memory tags for ARM MTE during swapping.

The algorithm is based on RLE and specifically targets 128-byte buffers of tags corresponding to a single page. In the common case a buffer can be compressed into 63 bits, making it possible to store it without additional memory allocation.

...

Documentation/arch/arm64/mte-tag-compression.rst line 211

Quoted 4 lines
Documentation/arch/arm64/mte-tag-compression.rst new file +4 -0
+Programming Interface
+=====================
+
+ .. kernel-doc:: arch/arm64/mm/mtecomp.c

:export:

Quoted 1 line

+

Is it dangling trailing blank line? Drop it.

...

arch/arm64/mm/mtecomp.c line 10

Quoted 3 lines
arch/arm64/mm/mtecomp.c new file +2 -0
+#include <linux/bitmap.h>
 
+#include <linux/bitops.h>

This is guaranteed to be included by bitmap.h.

Quoted 6 lines
arch/arm64/mm/mtecomp.c new file +6 -0
+#include <linux/export.h>
+#include <linux/gfp.h>
+#include <linux/module.h>
+#include <linux/slab.h>
+#include <linux/string.h>
+#include <linux/types.h>

...

Quoted 2 lines

+/*
+ * Sizes of compressed values. These depend on MTE_TAG_SIZE and

of the

Quoted 2 lines

+ * MTE_GRANULES_PER_PAGE.
+ */

...

arch/arm64/mm/mtecomp.c line 82

Quoted 6 lines
arch/arm64/mm/mtecomp.c new file +4 -0
+	u8 prev_tag = tags[0] / 16; /* First tag in the array. */
+	unsigned int cur_idx = 0, i, j;
+	u8 cur_tag;
 
 
+	out_tags[0] = prev_tag;

out_tags[cur_idx] ?

arch/arm64/mm/mtecomp.c line 92

Quoted 11 lines
arch/arm64/mm/mtecomp.c new file +10 -0
+	for (i = 0; i < MTE_PAGE_TAG_STORAGE; i++) {
+		for (j = 0; j < 2; j++) {
+			cur_tag = j ? (tags[i] % 16) : (tags[i] / 16);
+			if (cur_tag == prev_tag) {
+				out_sizes[cur_idx]++;
 
+			} else {
+				cur_idx++;
+				prev_tag = cur_tag;
+				out_tags[cur_idx] = prev_tag;
+				out_sizes[cur_idx] = 1;

Looking more at this I think there is still a room for improvement. I can't come up right now with a proposal (lunch time :-), but I would look into

	do {
		...
	} while (i < MTE_...);

approach.

Quoted 3 lines
arch/arm64/mm/mtecomp.c new file +3 -0
+			}
+		}
+	}

...

arch/arm64/mm/mtecomp.c line 137

Quoted 5 lines
arch/arm64/mm/mtecomp.c new file +4 -0
+static size_t mte_size_to_ranges(size_t size)
+{
+	size_t largest_bits;
 
+	size_t ret = 0;

Redundant assignment. Please, check again all of them.

arch/arm64/mm/mtecomp.c line 141

Quoted 4 lines
arch/arm64/mm/mtecomp.c new file +4 -0
+
+	largest_bits = (size == 8) ? MTE_BITS_PER_LARGEST_IDX_INLINE :
+				     MTE_BITS_PER_LARGEST_IDX;
+	ret = (size * 8 + MTE_BITS_PER_SIZE - largest_bits) /

Hmm... I thought that we moved BYTES_TO_BITS() to the generic header... Okay, never mind.

arch/arm64/mm/mtecomp.c line 143

Quoted 2 lines
arch/arm64/mm/mtecomp.c new file +2 -0
+	      (MTE_BITS_PER_TAG + MTE_BITS_PER_SIZE);
+	return ret;
	return (...) / ...;
Quoted 1 line

+}

...

arch/arm64/mm/mtecomp.c line 149

Quoted 3 lines
arch/arm64/mm/mtecomp.c new file +3 -0
+static size_t mte_alloc_size(unsigned int num_ranges)
+{
+	size_t sizes[4] = { 8, 16, 32, 64 };

Hooray! And now it's not needed anymore...

arch/arm64/mm/mtecomp.c line 152

Quoted 3 lines
arch/arm64/mm/mtecomp.c new file +3 -0
+	unsigned int i;
+
+	for (i = 0; i < ARRAY_SIZE(sizes); i++) {

...as sizes[i] is equivalent of (8 << i).

Quoted 7 lines
arch/arm64/mm/mtecomp.c new file +6 -0
+		if (num_ranges <= mte_size_to_ranges(sizes[i]))
+			return sizes[i];
+	}
+	return 128;
+}
 
+}

...

Quoted 6 lines

+/**
+ * mte_compress() - compress the given tag array.
+ * @tags: 128-byte array to read the tags from.
+ *
+ * Compresses the tags and returns a 64-bit opaque handle pointing to the
+ * tag storage. May allocate memory, which is freed by @mte_release_handle().

+ blank line here.

Quoted 2 lines

+ * Returns: 64-bit tag storage handle.
+ */

...

arch/arm64/mm/mtecomp.c line 245

Quoted 6 lines
arch/arm64/mm/mtecomp.c new file +6 -0
+	/*
+	 * mte_compress_to_buf() only initializes the bits that mte_decompress()
+	 * will read. But when the tags are stored in the handle itself, it must
+	 * have all its bits initialized.
+	 */
+	unsigned long result = 0;
	// Actually it's interesting how it's supposed to work on 32-bit
	// builds...
	DECLARE_BITMAP(result, BITS_PER_LONG);

and then

	bitmap_zero();

?

...

arch/arm64/mm/mtecomp.c line 276

Quoted 8 lines
arch/arm64/mm/mtecomp.c new file +8 -0
+static unsigned long mte_bitmap_read(const unsigned long *bitmap,
+				     unsigned long *pos, unsigned long bits)
+{
+	unsigned long result;
+
+	result = bitmap_read(bitmap, *pos, bits);
+	*pos += bits;
+	return result;
	unsigned long start = *pos;

	*pos += bits;
	return bitmap_read(bitmap, start, bits);
Quoted 1 line

+}

...

arch/arm64/mm/mtecomp.c line 283

Quoted 1 line
arch/arm64/mm/mtecomp.c new file +1 -0
+	unsigned short r_sizes[46], sum = 0;

See below.

...

It's cleaner and more robust to have

	sum = 0;

here.

Quoted 13 lines
arch/arm64/mm/mtecomp.c new file +13 -0
+	for (i = 0; i < max_ranges; i++) {
+		if (i == largest_idx)
+			continue;
+		r_sizes[i] =
+			mte_bitmap_read(bitmap, &bit_pos, MTE_BITS_PER_SIZE);
+		if (!r_sizes[i]) {
+			max_ranges = i;
+			break;
+		}
+		sum += r_sizes[i];
+	}
+	if (sum >= 256)
+		return false;
Signature · 2 lines
With Best Regards,
Andy Shevchenko
#22 lore raw

Re: [PATCH v4 3/5] arm64: mte: implement CONFIG_ARM64_MTE_COMP

Quoted 10 lines · Andy Shevchenko
Quoted 7 lines · Alexander Potapenko

The config implements the algorithm compressing memory tags for ARM MTE during swapping.

The algorithm is based on RLE and specifically targets 128-byte buffers of tags corresponding to a single page. In the common case a buffer can be compressed into 63 bits, making it possible to store it without additional memory allocation.

Quoted 8 lines

...

Documentation/arch/arm64/mte-tag-compression.rst line 211

Quoted 4 lines
Documentation/arch/arm64/mte-tag-compression.rst new file +4 -0
+Programming Interface
+=====================
+
+ .. kernel-doc:: arch/arm64/mm/mtecomp.c

:export:

Done

Quoted 3 lines
Quoted 1 line

+

Is it dangling trailing blank line? Drop it.

Sorry, it's hard to attribute this comment. I am assuming it is related to Documentation/arch/arm64/mte-tag-compression.rst - done.

Quoted 7 lines

...

arch/arm64/mm/mtecomp.c line 10

Quoted 3 lines
arch/arm64/mm/mtecomp.c new file +2 -0
+#include <linux/bitmap.h>
 
+#include <linux/bitops.h>

This is guaranteed to be included by bitmap.h.

I think we'd better stick to IWYU here.
Ingo's patch: https://git.kernel.org/pub/scm/linux/kernel/git/mingo/tip.git/commit/?id=32b1e9e4f5774951a3a80604a39fa1f0674c1833
specifically adds bitmap.h where bits.h is already present, without
removing the latter.
Although there might not be general consensus on this in the kernel
right now, I think Ingo's "Fast Kernel Headers" set out a good
direction.

Quoted 5 lines
Quoted 3 lines

+/*
+ * Sizes of compressed values. These depend on MTE_TAG_SIZE and

of the

This comment is gone now

Quoted 4 lines
Quoted 2 lines

+ out_tags[0] = prev_tag;

out_tags[cur_idx] ?

Yeah, looks more readable. Done.

Quoted 13 lines

arch/arm64/mm/mtecomp.c line 92

Quoted 12 lines
arch/arm64/mm/mtecomp.c new file +10 -0
+     for (i = 0; i < MTE_PAGE_TAG_STORAGE; i++) {
+             for (j = 0; j < 2; j++) {
+                     cur_tag = j ? (tags[i] % 16) : (tags[i] / 16);
+                     if (cur_tag == prev_tag) {
+                             out_sizes[cur_idx]++;
 
+                     } else {
+                             cur_idx++;
+                             prev_tag = cur_tag;
+                             out_tags[cur_idx] = prev_tag;
+                             out_sizes[cur_idx] = 1;
Quoted 8 lines

Looking more at this I think there is still a room for improvement. I can't come up right now with a proposal (lunch time :-), but I would look into

        do {
                ...
        } while (i < MTE_...);

approach.

We can e.g. get rid of the nested loop and iterate over tags instead of bytes (see v5)

Quoted 7 lines

arch/arm64/mm/mtecomp.c line 137

Quoted 5 lines
arch/arm64/mm/mtecomp.c new file +4 -0
+static size_t mte_size_to_ranges(size_t size)
+{
+     size_t largest_bits;
 
+     size_t ret = 0;

Redundant assignment. Please, check again all of them.

Done.

Quoted 7 lines

arch/arm64/mm/mtecomp.c line 141

Quoted 4 lines
arch/arm64/mm/mtecomp.c new file +4 -0
+
+     largest_bits = (size == 8) ? MTE_BITS_PER_LARGEST_IDX_INLINE :
+                                  MTE_BITS_PER_LARGEST_IDX;
+     ret = (size * 8 + MTE_BITS_PER_SIZE - largest_bits) /

Hmm... I thought that we moved BYTES_TO_BITS() to the generic header... Okay, never mind.

Ack

Quoted 4 lines

arch/arm64/mm/mtecomp.c line 143

Quoted 2 lines
arch/arm64/mm/mtecomp.c new file +2 -0
+           (MTE_BITS_PER_TAG + MTE_BITS_PER_SIZE);
+     return ret;
        return (...) / ...;

Done

Quoted 10 lines
Quoted 1 line

+}

...

arch/arm64/mm/mtecomp.c line 149

Quoted 3 lines
arch/arm64/mm/mtecomp.c new file +3 -0
+static size_t mte_alloc_size(unsigned int num_ranges)
+{
+     size_t sizes[4] = { 8, 16, 32, 64 };

Hooray! And now it's not needed anymore...

Quoted 5 lines

arch/arm64/mm/mtecomp.c line 152

Quoted 3 lines
arch/arm64/mm/mtecomp.c new file +3 -0
+     unsigned int i;
+
+     for (i = 0; i < ARRAY_SIZE(sizes); i++) {

...as sizes[i] is equivalent of (8 << i).

It's gone now.

Quoted 10 lines

...

Quoted 6 lines

+/**
+ * mte_compress() - compress the given tag array.
+ * @tags: 128-byte array to read the tags from.
+ *
+ * Compresses the tags and returns a 64-bit opaque handle pointing to the
+ * tag storage. May allocate memory, which is freed by @mte_release_handle().

+ blank line here.

Done (here and in other places in the file), but I'm wondering why
https://docs.kernel.org/doc-guide/kernel-doc.html does not mandate it.

Quoted 13 lines
Quoted 3 lines

+ * Returns: 64-bit tag storage handle.
+ */

...

arch/arm64/mm/mtecomp.c line 245

Quoted 6 lines
arch/arm64/mm/mtecomp.c new file +6 -0
+     /*
+      * mte_compress_to_buf() only initializes the bits that mte_decompress()
+      * will read. But when the tags are stored in the handle itself, it must
+      * have all its bits initialized.
+      */
+     unsigned long result = 0;
Quoted 2 lines
        // Actually it's interesting how it's supposed to work on 32-bit
        // builds...

It is not supposed to work on 32 bit.
First, the code is in arch/arm64 :)
Second, 32-bit CPUs do not support MTE (which reserves the four upper bits of the address)

Quoted 14 lines

arch/arm64/mm/mtecomp.c line 276

Quoted 9 lines
arch/arm64/mm/mtecomp.c new file +8 -0
+static unsigned long mte_bitmap_read(const unsigned long *bitmap,
+                                  unsigned long *pos, unsigned long bits)
+{
+     unsigned long result;
+
+     result = bitmap_read(bitmap, *pos, bits);
+     *pos += bits;
+     return result;
        unsigned long start = *pos;

        *pos += bits;
        return bitmap_read(bitmap, start, bits);

Done, thanks!

Quoted 8 lines
Quoted 1 line

+}

...

Quoted 1 line

+ unsigned short r_sizes[46], sum = 0;

See below.

Quoted 7 lines

...

It's cleaner and more robust to have

        sum = 0;

here.

Moved it inside the loop init statement

Quoted 4 lines

--
With Best Regards,
Andy Shevchenko

Thank you!

_______________________________________________
linux-arm-kernel mailing list
[email protected]
http://lists.infradead.org/mailman/listinfo/linux-arm-kernel

maintainer
#23 lore raw

Re: [PATCH v4 3/5] arm64: mte: implement CONFIG_ARM64_MTE_COMP

In reply to Alexander Potapenko

Just some random comments, I haven't reviewed the series properly.

Quoted 16 lines · Alexander Potapenko
Documentation/arch/arm64/mte-tag-compression.rst new file +10 -0
new file mode 100644
index 0000000000000..af6716d53c1a8
@@ -0,0 +1,212 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+==================================================
+Tag Compression for Memory Tagging Extension (MTE)
+==================================================
+
+This document describes the algorithm used to compress memory tags used by the
+ARM Memory Tagging Extension (MTE)
+
+Introduction

Documentation/arch/arm64/mte-tag-compression.rst line 20

Quoted 10 lines
Documentation/arch/arm64/mte-tag-compression.rst new file +10 -0
+============
+
+MTE assigns tags to memory pages: for 4K pages those tags occupy 128 bytes
+(256 4-bit tags each corresponding to a 16-byte MTE granule). By default, MTE
+carves out 3.125% (1/16) of the available physical memory to store the tags.
+
+When MTE pages are saved to swap, their tags need to be stored in the kernel
+memory. If the system swap is used heavily, these tags may take a substantial
+portion of the physical memory, which in the case of a zram-backed swap may
+even exceed the memory used to store the swapped pages themselves.

Hmm, I'm not sure about this claim ;). Is the zram so good that it manages a 32x compression (4096/128)?

How much would we save if we only do the compression when it can fit in 63 bits?

Quoted 25 lines
Documentation/arch/arm64/mte-tag-compression.rst new file +25 -0
+/**
+ * mte_tags_to_ranges() - break @tags into arrays of tag ranges.
+ * @tags: 128-byte array containing 256 MTE tags.
+ * @out_tags: u8 array to store the tag of every range.
+ * @out_sizes: unsigned short array to store the size of every range.
+ * @out_len: length of @out_tags and @out_sizes (output parameter, initially
+ *           equal to lengths of out_tags[] and out_sizes[]).
+ */
+void mte_tags_to_ranges(u8 *tags, u8 *out_tags, unsigned short *out_sizes,
+			size_t *out_len)
+{
+	u8 prev_tag = tags[0] / 16; /* First tag in the array. */
+	unsigned int cur_idx = 0, i, j;
+	u8 cur_tag;
+
+	memset(out_tags, 0, array_size(*out_len, sizeof(*out_tags)));
+	memset(out_sizes, 0, array_size(*out_len, sizeof(*out_sizes)));
+
+	out_tags[0] = prev_tag;
+	for (i = 0; i < MTE_PAGE_TAG_STORAGE; i++) {
+		for (j = 0; j < 2; j++) {
+			cur_tag = j ? (tags[i] % 16) : (tags[i] / 16);
+			if (cur_tag == prev_tag) {
+				out_sizes[cur_idx]++;
+			} else {

Documentation/arch/arm64/mte-tag-compression.rst line 55

Quoted 10 lines
Documentation/arch/arm64/mte-tag-compression.rst new file +10 -0
+				cur_idx++;
+				prev_tag = cur_tag;
+				out_tags[cur_idx] = prev_tag;
+				out_sizes[cur_idx] = 1;
+			}
+		}
+	}
+	*out_len = cur_idx + 1;
+}
+EXPORT_SYMBOL_NS(mte_tags_to_ranges, MTECOMP);

What's with the exports here? Are we expecting these functions to be called from loadable modules?

Signature · 6 lines
Catalin

_______________________________________________
linux-arm-kernel mailing list
[email protected]
http://lists.infradead.org/mailman/listinfo/linux-arm-kernel
#24 lore raw

Re: [PATCH v4 3/5] arm64: mte: implement CONFIG_ARM64_MTE_COMP

Quoted 11 lines

Documentation/arch/arm64/mte-tag-compression.rst

Quoted 8 lines
Documentation/arch/arm64/mte-tag-compression.rst new file +8 -0
+MTE assigns tags to memory pages: for 4K pages those tags occupy 128 bytes
+(256 4-bit tags each corresponding to a 16-byte MTE granule). By default, MTE
+carves out 3.125% (1/16) of the available physical memory to store the tags.
+
+When MTE pages are saved to swap, their tags need to be stored in the kernel
+memory. If the system swap is used heavily, these tags may take a substantial
+portion of the physical memory, which in the case of a zram-backed swap may
+even exceed the memory used to store the swapped pages themselves.

Hmm, I'm not sure about this claim ;). Is the zram so good that it manages a 32x compression (4096/128)?

For trivial data, like zero pages, this is possible in theory, but I am not sure whether zram takes advantage of this. I removed the claim :)

Quoted 3 lines

How much would we save if we only do the compression when it can fit in 63 bits?

Good question. What I've observed is that soon after boot roughly 50% of tag buffers can be compressed into 63 bits.

Quoted 3 lines
Quoted 3 lines
arch/arm64/mm/mtecomp.c new file +3 -0
+void mte_tags_to_ranges(u8 *tags, u8 *out_tags, unsigned short *out_sizes,
+                     size_t *out_len)
+{

...

Quoted 5 lines

Review

Quoted 2 lines
(quoted diff) +2 -0
+}
+EXPORT_SYMBOL_NS(mte_tags_to_ranges, MTECOMP);

What's with the exports here? Are we expecting these functions to be called from loadable modules?

Yes, this is for the KUnit test module

Quoted 3 lines

--
Catalin

Signature · 15 lines
Alexander Potapenko
Software Engineer

Google Germany GmbH
Erika-Mann-Straße, 33
80636 München

Geschäftsführer: Paul Manicle, Liana Sebastian
Registergericht und -nummer: Hamburg, HRB 86891
Sitz der Gesellschaft: Hamburg

_______________________________________________
linux-arm-kernel mailing list
[email protected]
http://lists.infradead.org/mailman/listinfo/linux-arm-kernel
#25 lore raw

PATCH v4 4/5 arm64: mte: add a test for MTE tags compression

In reply to Alexander Potapenko

Ensure that tag sequences containing alternating values are compressed to buffers of expected size and correctly decompressed afterwards.

---
v4:
- addressed comments by Andy Shevchenko:
- expanded MTE to "Memory Tagging Extension" in Kconfig
- changed signed variables to unsigned where applicable
- added missing header dependencies

- addressed comments by Yury Norov:
- moved test-only declarations from mtecomp.h into this test
- switched to the new "mte"-prefixed function names, dropped the mentions of "EA0"
- added test_tag_to_ranges_n()

v3:
- addressed comments by Andy Shevchenko in another patch:
- switched from u64 to unsigned long
- added MODULE_IMPORT_NS(MTECOMP)
- fixed includes order

---
 arch/arm64/Kconfig           |  10 ++
 arch/arm64/mm/Makefile       |   1 +
 arch/arm64/mm/test_mtecomp.c | 217 +++++++++++++++++++++++++++++++++++
 3 files changed, 228 insertions(+)
 create mode 100644 arch/arm64/mm/test_mtecomp.c
arch/arm64/Kconfig +10 -0
index 3ac6e302b1509..2432475003054 100644
@@ -2102,6 +2102,16 @@ config ARM64_MTE_COMP
 
 	  128-byte tag buffers corresponding to 4K pages can be compressed to save heap memory.
 
+config ARM64_MTE_COMP_KUNIT_TEST
+	tristate "Test tag compression for ARM64 Memory Tagging Extension" if !KUNIT_ALL_TESTS
+	default KUNIT_ALL_TESTS
+	depends on KUNIT && ARM64_MTE_COMP
+	help
+	  Test MTE compression algorithm enabled by CONFIG_ARM64_MTE_COMP.
+
+	  Ensure that tag sequences containing alternating values are compressed
+	  to buffers of expected size and correctly decompressed afterwards.
+
 config ARM64_SVE
 	bool "ARM Scalable Vector Extension support"
 	default y
arch/arm64/mm/Makefile +1 -0
index 46778f6dd83c2..170dc62b010b9 100644
@@ -11,6 +11,7 @@ obj-$(CONFIG_TRANS_TABLE)	+= trans_pgd-asm.o
 obj-$(CONFIG_DEBUG_VIRTUAL)	+= physaddr.o
 obj-$(CONFIG_ARM64_MTE)		+= mteswap.o
 obj-$(CONFIG_ARM64_MTE_COMP)	+= mtecomp.o
+obj-$(CONFIG_ARM64_MTE_COMP_KUNIT_TEST) += test_mtecomp.o
 KASAN_SANITIZE_physaddr.o	+= n
 
 obj-$(CONFIG_KASAN)		+= kasan_init.o
arch/arm64/mm/test_mtecomp.c new file +217 -0

217 lines not on this page · raw message

Signature · 1 line
2.41.0.487.g6d72f3e995-goog
#26 lore raw

Re: [PATCH v4 4/5] arm64: mte: add a test for MTE tags compression

Quoted 2 lines · Alexander Potapenko

Ensure that tag sequences containing alternating values are compressed to buffers of expected size and correctly decompressed afterwards.

...

arch/arm64/mm/test_mtecomp.c line 10

Quoted 6 lines
arch/arm64/mm/test_mtecomp.c new file +5 -0
+#include <linux/bits.h>
+#include <linux/module.h>
+#include <linux/slab.h>
+#include <linux/string.h>
 
+#include <kunit/test.h>

Keep this in a separate group outside of linux/*.

Quoted 3 lines
arch/arm64/mm/test_mtecomp.c new file +3 -0
+#include <linux/types.h>
+
+#include <asm/mtecomp.h>

...

arch/arm64/mm/test_mtecomp.c line 19

Quoted 5 lines
arch/arm64/mm/test_mtecomp.c new file +5 -0
+/* Functions exported from mtecomp.c for this test. */
+void mte_tags_to_ranges(u8 *tags, u8 *out_tags, unsigned short *out_sizes,
+			size_t *out_len);
+void mte_ranges_to_tags(u8 *r_tags, unsigned short *r_sizes, size_t r_len,
+			u8 *tags);

This is interesting. Have you run make W=1 on this code?

Signature · 2 lines
With Best Regards,
Andy Shevchenko
#27 lore raw

Re: [PATCH v4 4/5] arm64: mte: add a test for MTE tags compression

Quoted 7 lines · Andy Shevchenko
Quoted 2 lines · Alexander Potapenko

Ensure that tag sequences containing alternating values are compressed to buffers of expected size and correctly decompressed afterwards.

...

Quoted 8 lines

arch/arm64/mm/test_mtecomp.c line 10

Quoted 6 lines
arch/arm64/mm/test_mtecomp.c new file +5 -0
+#include <linux/bits.h>
+#include <linux/module.h>
+#include <linux/slab.h>
+#include <linux/string.h>
 
+#include <kunit/test.h>

Keep this in a separate group outside of linux/*.

Done.

Quoted 7 lines
Quoted 4 lines
arch/arm64/mm/test_mtecomp.c new file +3 -0
+#include <linux/types.h>
+
+#include <asm/mtecomp.h>

...

Quoted 7 lines

arch/arm64/mm/test_mtecomp.c line 19

Quoted 5 lines
arch/arm64/mm/test_mtecomp.c new file +5 -0
+/* Functions exported from mtecomp.c for this test. */
+void mte_tags_to_ranges(u8 *tags, u8 *out_tags, unsigned short *out_sizes,
+                     size_t *out_len);
+void mte_ranges_to_tags(u8 *r_tags, unsigned short *r_sizes, size_t r_len,
+                     u8 *tags);

This is interesting. Have you run make W=1 on this code?

You are right, this is fishy. I added mtecomp.h and moved the declarations there instead.

#28 lore raw

PATCH v4 5/5 arm64: mte: add compression support to mteswap.c

In reply to Alexander Potapenko

Define the internal mteswap.h interface:
- _mte_alloc_and_save_tags()
- _mte_free_saved_tags()
- _mte_restore_tags()

, that encapsulates saving tags for a struct page (together with memory allocation), restoring tags, and deleting the storage allocated for them.

These functions accept opaque pointers, which may point to 128-byte tag buffers, as well as smaller buffers containing compressed tags, or have compressed tags stored directly in them.

The existing code from mteswap.c operating with uncompressed tags is split away into mteswap_nocomp.c, and the newly introduced mteswap_comp.c provides compression support. The latter implementation
is picked if CONFIG_ARM64_MTE_COMP=y.

Soon after booting Android, tag compression saves ~2.5x memory previously spent by mteswap.c on tag allocations. With the growing uptime, the savings reach 20x and even more.

---
v4:
- minor code simplifications suggested by Andy Shevchenko, added missing header dependencies
- changed compression API names to reflect modifications made to memcomp.h (as suggested by Yury Norov)

v3:
- Addressed comments by Andy Shevchenko in another patch:
- fixed includes order
- replaced u64 with unsigned long
- added MODULE_IMPORT_NS(MTECOMP)

---
 arch/arm64/mm/Makefile         |  5 ++++
 arch/arm64/mm/mteswap.c        | 20 ++++++-------
 arch/arm64/mm/mteswap.h        | 12 ++++++++
 arch/arm64/mm/mteswap_comp.c   | 54 ++++++++++++++++++++++++++++++++++
 arch/arm64/mm/mteswap_nocomp.c | 38 ++++++++++++++++++++++++
 5 files changed, 118 insertions(+), 11 deletions(-)
 create mode 100644 arch/arm64/mm/mteswap.h
 create mode 100644 arch/arm64/mm/mteswap_comp.c
 create mode 100644 arch/arm64/mm/mteswap_nocomp.c
arch/arm64/mm/Makefile +5 -0
index 170dc62b010b9..46a798e2b67cb 100644
@@ -11,6 +11,11 @@ obj-$(CONFIG_TRANS_TABLE)	+= trans_pgd-asm.o
 obj-$(CONFIG_DEBUG_VIRTUAL)	+= physaddr.o
 obj-$(CONFIG_ARM64_MTE)		+= mteswap.o
 obj-$(CONFIG_ARM64_MTE_COMP)	+= mtecomp.o
+ifdef CONFIG_ARM64_MTE_COMP
+obj-$(CONFIG_ARM64_MTE)		+= mteswap_comp.o
+else
+obj-$(CONFIG_ARM64_MTE)		+= mteswap_nocomp.o
+endif
 obj-$(CONFIG_ARM64_MTE_COMP_KUNIT_TEST) += test_mtecomp.o
 KASAN_SANITIZE_physaddr.o	+= n
 
arch/arm64/mm/mteswap.c +9 -11
index cd508ba80ab1b..9d8f87fd191a2 100644
@@ -5,8 +5,11 @@
 #include <linux/slab.h>
 #include <linux/swap.h>
 #include <linux/swapops.h>
+
 #include <asm/mte.h>
 
+#include "mteswap.h"
+
 static DEFINE_XARRAY(mte_pages);
 
 void *mte_allocate_tag_storage(void)
@@ -27,20 +30,18 @@ int mte_save_tags(struct page *page)
 	if (!page_mte_tagged(page))
 		return 0;
 
-	tag_storage = mte_allocate_tag_storage();
+	tag_storage = _mte_alloc_and_save_tags(page);
 	if (!tag_storage)
 		return -ENOMEM;
 
-	mte_save_page_tags(page_address(page), tag_storage);
-
 	/* page_private contains the swap entry.val set in do_swap_page */
 	ret = xa_store(&mte_pages, page_private(page), tag_storage, GFP_KERNEL);
 	if (WARN(xa_is_err(ret), "Failed to store MTE tags")) {
-		mte_free_tag_storage(tag_storage);
+		_mte_free_saved_tags(tag_storage);
 		return xa_err(ret);
 	} else if (ret) {
 		/* Entry is being replaced, free the old entry */
-		mte_free_tag_storage(ret);
+		_mte_free_saved_tags(ret);
 	}
 
 	return 0;
@@ -53,10 +54,7 @@ void mte_restore_tags(swp_entry_t entry, struct page *page)
 	if (!tags)
 		return;
 
-	if (try_page_mte_tagging(page)) {
-		mte_restore_page_tags(page_address(page), tags);
-		set_page_mte_tagged(page);
-	}
+	_mte_restore_tags(tags, page);
 }
 
 void mte_invalidate_tags(int type, pgoff_t offset)
@@ -64,7 +62,7 @@ void mte_invalidate_tags(int type, pgoff_t offset)
 	swp_entry_t entry = swp_entry(type, offset);
 	void *tags = xa_erase(&mte_pages, entry.val);
 
-	mte_free_tag_storage(tags);
+	_mte_free_saved_tags(tags);
 }
 
 void mte_invalidate_tags_area(int type)
@@ -78,7 +76,7 @@ void mte_invalidate_tags_area(int type)
 	xa_lock(&mte_pages);
 	xas_for_each(&xa_state, tags, last_entry.val - 1) {
 		__xa_erase(&mte_pages, xa_state.xa_index);
-		mte_free_tag_storage(tags);
+		_mte_free_saved_tags(tags);
 	}
 	xa_unlock(&mte_pages);
 }
arch/arm64/mm/mteswap.h new file +12 -0
new file mode 100644
index 0000000000000..4c576b76785d1
@@ -0,0 +1,12 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+
+#ifndef ARCH_ARM64_MM_MTESWAP_H_
+#define ARCH_ARM64_MM_MTESWAP_H_
+
+struct page;
+
+void *_mte_alloc_and_save_tags(struct page *page);
+void _mte_free_saved_tags(void *tags);
+void _mte_restore_tags(void *tags, struct page *page);
+
+#endif // ARCH_ARM64_MM_MTESWAP_H_
arch/arm64/mm/mteswap_comp.c new file +54 -0

54 lines not on this page · raw message

arch/arm64/mm/mteswap_nocomp.c new file +38 -0
new file mode 100644
index 0000000000000..1e665a4b5f940
@@ -0,0 +1,38 @@
+// SPDX-License-Identifier: GPL-2.0
+
+/* MTE tag storage management without compression support. */
+
+#include <linux/pagemap.h>
+#include <linux/slab.h>
+#include <linux/swap.h>
+#include <linux/swapops.h>
+#include <linux/xarray.h>
+
+#include <asm/mte.h>
+
+#include "mteswap.h"
+
+void *_mte_alloc_and_save_tags(struct page *page)
+{
+	void *storage;
+
+	storage = mte_allocate_tag_storage();
+	if (!storage)
+		return NULL;
+
+	mte_save_page_tags(page_address(page), storage);
+	return storage;
+}
+
+void _mte_free_saved_tags(void *storage)
+{
+	mte_free_tag_storage(storage);
+}
+
+void _mte_restore_tags(void *tags, struct page *page)
+{
+	if (!try_page_mte_tagging(page))
+		return;
+	mte_restore_page_tags(page_address(page), tags);
+	set_page_mte_tagged(page);
+}
Signature · 1 line
2.41.0.487.g6d72f3e995-goog
maintainer
#29 lore raw

Re: [PATCH v4 5/5] arm64: mte: add compression support to mteswap.c

Quoted 3 lines · Alexander Potapenko

Soon after booting Android, tag compression saves ~2.5x memory previously spent by mteswap.c on tag allocations. With the growing uptime, the savings reach 20x and even more.

This sounds like a marketing claim ;). The way I read your statement is that the memory used for tag storage is 2.5x less with your patches and that's great. It means a 2.5x compression on average. How does the compression get so much better to 20x with more uptime? The number of tag storage allocations should be proportional to the pages swapped out (not equal as not all would be mapped as PROT_MTE). So you can indeed have a lot more pages swapped out than available in RAM and the tag storage can take space but not sure which numbers you divided to get 20x.

Anyway, it would be nice to see the full picture of what the savings relative to the total RAM is. Given that the swap in this instance is zram, you have an upper bound of how many pages it can store. I'm just trying to assess whether the complexity added here is worth it.

Maybe not as good as the RLE algorithm here, I was wondering whether we could use zswap to save the tags together with the page. I looked some time ago at it seemed slightly easier for zswap than zram. Another option is to make the swap format more generic to support metadata storage. Yet another option is for the zram to flag that it can compress the metadata together with the data (no swap format change needed; when decompressing the page, it populates the tags as well).

Signature · 6 lines
Catalin

_______________________________________________
linux-arm-kernel mailing list
[email protected]
http://lists.infradead.org/mailman/listinfo/linux-arm-kernel
#30 lore raw

Re: [PATCH v4 5/5] arm64: mte: add compression support to mteswap.c

Quoted 10 lines · Catalin Marinas
Quoted 3 lines · Alexander Potapenko

Soon after booting Android, tag compression saves ~2.5x memory previously spent by mteswap.c on tag allocations. With the growing uptime, the savings reach 20x and even more.

This sounds like a marketing claim ;). The way I read your statement is that the memory used for tag storage is 2.5x less with your patches and that's great. It means a 2.5x compression on average. How does the compression get so much better to 20x with more uptime?

I am currently looking at this, and I think this happens because some userspace apps assign zero tags to userspace pages, and those tags are effectively zero-cost, because they can be compressed into 8 bytes of the Xarray pointer.
As the uptime grows, the share of such pages also grows.
I agree though that this is a marginal use case, and the average compression rate is more representative.

Quoted 3 lines

The number of
tag storage allocations should be proportional to the pages swapped out (not equal as not all would be mapped as PROT_MTE).

We think a reasonable upper bound for PROT_MTE pages is 40%, but currently it is probably more like 20%.

Quoted 4 lines

So you can indeed
have a lot more pages swapped out than available in RAM and the tag storage can take space but not sure which numbers you divided to get 20x.

Right now (in kernel 6.5) the amount of memory spent to store the tags can be calculated as 128 * (number of mte_allocate_tag_storage() calls
- number of mte_free_tag_storage() calls).
In my patchset I calculate the total amount of memory allocated/deallocated from the mte-tags-N caches and compare that with 128 * (total number of live objects in those caches).
E.g. the stats after booting up the device (~120s uptime) look as follows:

8 bytes: 14007 allocations, 256 deallocations
16 bytes: 1583 allocations, 179 deallocations
32 bytes: 1634 allocations, 205 deallocations
64 bytes: 1538 allocations, 142 deallocations
128 bytes: 10881 allocations, 1340 deallocations
uncompressed tag storage size: 3522688
compressed tag storage size: 1488792

(note 8-byte allocations contribute to uncompressed storage, but not to compressed storage).

After running various apps, I made the device use almost 19Mb swap space:
8 bytes: 71352 allocations, 8093 deallocations
16 bytes: 5102 allocations, 2598 deallocations
32 bytes: 8206 allocations, 4536 deallocations
64 bytes: 9489 allocations, 5377 deallocations
128 bytes: 43258 allocations, 23364 deallocations uncompressed tag storage size: 11960192
compressed tag storage size: 2967104

(Note the share of allocations compressed into 8 bytes is slowly growing)

In this case the compression ratio is 4x. I must admit I could not reproduce the 20x compression this time, and after thinking a little I anticipate the absolute values to be lower in that case (maybe that was observed at some point where a lot of uncompressed data was evicted from swap).

I therefore think I'd better make a modester claim in the docs/patch description.

Quoted 14 lines

Anyway, it would be nice to see the full picture of what the savings relative to the total RAM is. Given that the swap in this instance is zram, you have an upper bound of how many pages it can store. I'm just trying to assess whether the complexity added here is worth it.

Maybe not as good as the RLE algorithm here, I was wondering whether we could use zswap to save the tags together with the page. I looked some time ago at it seemed slightly easier for zswap than zram. Another option is to make the swap format more generic to support metadata storage. Yet another option is for the zram to flag that it can compress the metadata together with the data (no swap format change needed; when decompressing the page, it populates the tags as well).

Quoted 2 lines

--
Catalin

--
Alexander Potapenko
Software Engineer

Google Germany GmbH
Erika-Mann-Straße, 33
80636 München

Geschäftsführer: Paul Manicle, Liana Sebastian
Registergericht und -nummer: Hamburg, HRB 86891
Sitz der Gesellschaft: Hamburg

#31 lore raw

Re: [PATCH v4 5/5] arm64: mte: add compression support to mteswap.c

Quoted 50 lines · Alexander Potapenko
Quoted 10 lines · Catalin Marinas
Quoted 3 lines · Alexander Potapenko

Soon after booting Android, tag compression saves ~2.5x memory previously spent by mteswap.c on tag allocations. With the growing uptime, the savings reach 20x and even more.

This sounds like a marketing claim ;). The way I read your statement is that the memory used for tag storage is 2.5x less with your patches and that's great. It means a 2.5x compression on average. How does the compression get so much better to 20x with more uptime?

I am currently looking at this, and I think this happens because some userspace apps assign zero tags to userspace pages, and those tags are effectively zero-cost, because they can be compressed into 8 bytes of the Xarray pointer.
As the uptime grows, the share of such pages also grows.
I agree though that this is a marginal use case, and the average compression rate is more representative.

Quoted 3 lines

The number of
tag storage allocations should be proportional to the pages swapped out (not equal as not all would be mapped as PROT_MTE).

We think a reasonable upper bound for PROT_MTE pages is 40%, but currently it is probably more like 20%.

Quoted 4 lines

So you can indeed
have a lot more pages swapped out than available in RAM and the tag storage can take space but not sure which numbers you divided to get 20x.

Right now (in kernel 6.5) the amount of memory spent to store the tags can be calculated as 128 * (number of mte_allocate_tag_storage() calls
- number of mte_free_tag_storage() calls).
In my patchset I calculate the total amount of memory allocated/deallocated from the mte-tags-N caches and compare that with 128 * (total number of live objects in those caches).
E.g. the stats after booting up the device (~120s uptime) look as follows:

8 bytes: 14007 allocations, 256 deallocations
16 bytes: 1583 allocations, 179 deallocations
32 bytes: 1634 allocations, 205 deallocations
64 bytes: 1538 allocations, 142 deallocations
128 bytes: 10881 allocations, 1340 deallocations
uncompressed tag storage size: 3522688
compressed tag storage size: 1488792

Quoted 4 lines

(note 8-byte allocations contribute to uncompressed storage, but not to compressed storage).

After running various apps, I made the device use almost 19Mb swap space:

Sorry, this is by no means 19Mb

Quoted 7 lines

8 bytes: 71352 allocations, 8093 deallocations
16 bytes: 5102 allocations, 2598 deallocations
32 bytes: 8206 allocations, 4536 deallocations
64 bytes: 9489 allocations, 5377 deallocations
128 bytes: 43258 allocations, 23364 deallocations
uncompressed tag storage size: 11960192
compressed tag storage size: 2967104

#32 lore raw

Re: [PATCH v4 5/5] arm64: mte: add compression support to mteswap.c

In reply to Catalin Marinas
Quoted 5 lines

Anyway, it would be nice to see the full picture of what the savings relative to the total RAM is. Given that the swap in this instance is zram, you have an upper bound of how many pages it can store. I'm just trying to assess whether the complexity added here is worth it.

Assuming the average compression rate of 2.5x, and the share of tagged pages being 20%, we'll be saving 0.375% of the swapped memory:
20% / 32 * (2.5-1)/2.5
With the compression rate of 4x and 40% of PROT_MTE pages, that would be 0.9375%, which is tens of megabytes (probably still a little though).

Quoted 4 lines

Maybe not as good as the RLE algorithm here, I was wondering whether we could use zswap to save the tags together with the page. I looked some time ago at it seemed slightly easier for zswap than zram.

Android uses zram, so unfortunately we'll need to handle both.

Quoted 5 lines

Another
option is to make the swap format more generic to support metadata storage. Yet another option is for the zram to flag that it can compress the metadata together with the data (no swap format change needed; when decompressing the page, it populates the tags as well).

I haven't looked into this, but this might sound promising. We'll have to copy the page contents to a temporary buffer holding both the page data and the metadata, but it might be ok.

Another idea you've mentioned in one of the other patches is to only compress the data if it fits into 63 bits.
Looks like this alone could yield 2x+ compression for 4K pages, and will spare us the kmem cache juggling in this patch series.

Quoted 2 lines

--
Catalin

--
Alexander Potapenko
Software Engineer

Google Germany GmbH
Erika-Mann-Straße, 33
80636 München

Geschäftsführer: Paul Manicle, Liana Sebastian
Registergericht und -nummer: Hamburg, HRB 86891
Sitz der Gesellschaft: Hamburg