Ping^3. On Thu, 14 Dec 2023, Alexander Monakov wrote:
> Ping^2. > > On Thu, 9 Nov 2023, Alexander Monakov wrote: > > > I'd like to ping this patch on behalf of Mikhail. > > > > https://patchew.org/QEMU/20231027143704.7060-1-mmroma...@ispras.ru/ > > > > If this needs to be split up a bit to ease review, please let us know. > > > > On Fri, 27 Oct 2023, Mikhail Romanov wrote: > > > > > Improve buffer_is_zero function which is often used in qemu-img utility. > > > For instance, when converting a 4.4 GiB Windows 10 image to qcow2 it > > > takes around 40% of qemu-img run time (measured with 'perf record'). > > > > > > * The main improvements: > > > > > > 1) Define an inline wrapper for this function in include/qemu/cutils.h. > > > It checks three bytes from the buffer, avoiding call overhead when > > > any of those is non-zero. > > > > > > 2) Move the decision between accelerators to the inline wrapper so it > > > can be optimized out when buffer size is known at compile time. > > > > > > * Cleanups: > > > > > > 3) Delete AVX-512 accelerator, which is now invoked rarely thanks to > > > inline wrapper, so its speed benefit is neutralized by processor > > > frequency and voltage transition periods, as described in > > > https://travisdowns.github.io/blog/2020/01/17/avxfreq1.html > > > > > > 4) Delete SSE4 accelerator because its only difference with the SSE2 one > > > is using ptest instead of pcmpeq+pmovmsk to compare a vector with 0, but > > > it gives no perfomance benefit (according to uops.info data). > > > > > > 5) Remove all prefetches because they are done just a few processor > > > cycles before their target would be loaded. > > > > > > * Improvements for SIMD variants: > > > > > > 6) Double amount of bytes checked in an iteration of the main loop in > > > both SSE2 and AVX2 accelerators, moving the bottleneck from ALU port > > > contention to load ports (two loads per cycle on popular x86 > > > implementations). The improvement can be seen on real CPUs as well as > > > uiCA simulation. > > > > > > 7) Replace unaligned tail checking in AVX2 accelerator with aligned tail > > > checking similar to SSE2's one because reading unaligned tail gives no > > > benefit. > > > > > > 8) Move tail checking in both SSE2 and AVX2 accelerators before the main > > > loop so pcmpeq+pmovmsk checks are spread out more evenly. > > > > > > * Correctness fixes: > > > > > > 9) Add uint64_a type for pointers in integer version so they can alias > > > with any other type used in the buffer. > > > > > > 10) Adjust loop iterators to avoid incrementing a pointer past the end of > > > the buffer. > > > > > > * Other improvements: > > > > > > 11) Improve checking buffers with len < 8 in internal integer function > > > because inline wrapper ensures len >= 4. > > > > > > After these improvements buffer_is_zero works ~40% faster and takes 28% > > > of qemu-img run time (measured the same way as initial version, inline > > > wrapper execution included). > > > > > > The test-bufferiszero.c unit test still passes. > > > > > > Signed-off-by: Mikhail Romanov <mmroma...@ispras.ru> > > > --- > > > > > > v2: reworded the commit message and comments; use casts via 'void *' > > > > > > As buffer_is_zero is now a static inline function, should it be moved > > > into its > > > own header file? > > > > > > include/qemu/cutils.h | 25 ++++- > > > util/bufferiszero.c | 249 +++++++++++++++++------------------------- > > > 2 files changed, 122 insertions(+), 152 deletions(-) > > > > > > diff --git a/include/qemu/cutils.h b/include/qemu/cutils.h > > > index 92c927a6a3..6e35802b5e 100644 > > > --- a/include/qemu/cutils.h > > > +++ b/include/qemu/cutils.h > > > @@ -187,7 +187,30 @@ char *freq_to_str(uint64_t freq_hz); > > > /* used to print char* safely */ > > > #define STR_OR_NULL(str) ((str) ? (str) : "null") > > > > > > -bool buffer_is_zero(const void *buf, size_t len); > > > +bool buffer_is_zero_len_4_plus(const void *buf, size_t len); > > > +extern bool (*buffer_is_zero_len_256_plus)(const void *, size_t); > > > +static inline bool buffer_is_zero(const void *vbuf, size_t len) > > > +{ > > > + const char *buf = vbuf; > > > + > > > + if (len == 0) { > > > + return true; > > > + } > > > + if (buf[0] || buf[len - 1] || buf[len / 2]) { > > > + return false; > > > + } > > > + /* For len <= 3, all bytes are already tested. */ > > > + if (len <= 3) { > > > + return true; > > > + } > > > + > > > + if (len >= 256) { > > > + return buffer_is_zero_len_256_plus(vbuf, len); > > > + } else { > > > + return buffer_is_zero_len_4_plus(vbuf, len); > > > + } > > > +} > > > + > > > bool test_buffer_is_zero_next_accel(void); > > > > > > /* > > > diff --git a/util/bufferiszero.c b/util/bufferiszero.c > > > index 3e6a5dfd63..3e5a014368 100644 > > > --- a/util/bufferiszero.c > > > +++ b/util/bufferiszero.c > > > @@ -26,30 +26,23 @@ > > > #include "qemu/bswap.h" > > > #include "host/cpuinfo.h" > > > > > > -static bool > > > -buffer_zero_int(const void *buf, size_t len) > > > +typedef uint64_t uint64_a __attribute__((may_alias)); > > > + > > > +bool > > > +buffer_is_zero_len_4_plus(const void *buf, size_t len) > > > { > > > if (unlikely(len < 8)) { > > > - /* For a very small buffer, simply accumulate all the bytes. */ > > > - const unsigned char *p = buf; > > > - const unsigned char *e = buf + len; > > > - unsigned char t = 0; > > > - > > > - do { > > > - t |= *p++; > > > - } while (p < e); > > > - > > > - return t == 0; > > > + /* Inline wrapper ensures len >= 4. */ > > > + return (ldl_he_p(buf) | ldl_he_p(buf + len - 4)) == 0; > > > } else { > > > - /* Otherwise, use the unaligned memory access functions to > > > - handle the beginning and end of the buffer, with a couple > > > + /* Use unaligned memory access functions to handle > > > + the beginning and end of the buffer, with a couple > > > of loops handling the middle aligned section. */ > > > - uint64_t t = ldq_he_p(buf); > > > - const uint64_t *p = (uint64_t *)(((uintptr_t)buf + 8) & -8); > > > - const uint64_t *e = (uint64_t *)(((uintptr_t)buf + len) & -8); > > > + uint64_t t = ldq_he_p(buf) | ldq_he_p(buf + len - 8); > > > + const uint64_a *p = (void *)(((uintptr_t)buf + 8) & -8); > > > + const uint64_a *e = (void *)(((uintptr_t)buf + len) & -8); > > > > > > - for (; p + 8 <= e; p += 8) { > > > - __builtin_prefetch(p + 8); > > > + for (; p < e - 7; p += 8) { > > > if (t) { > > > return false; > > > } > > > @@ -58,7 +51,6 @@ buffer_zero_int(const void *buf, size_t len) > > > while (p < e) { > > > t |= *p++; > > > } > > > - t |= ldq_he_p(buf + len - 8); > > > > > > return t == 0; > > > } > > > @@ -67,124 +59,112 @@ buffer_zero_int(const void *buf, size_t len) > > > #if defined(CONFIG_AVX512F_OPT) || defined(CONFIG_AVX2_OPT) || > > > defined(__SSE2__) > > > #include <immintrin.h> > > > > > > -/* Note that each of these vectorized functions require len >= 64. */ > > > +/* Prevent the compiler from reassociating > > > + a chain of similar operations. */ > > > +#define SSE_REASSOC_BARRIER(a, b) asm("" : "+x"(a), "+x"(b)) > > > + > > > +/* Note that each of these vectorized functions assume len >= 256. */ > > > > > > static bool __attribute__((target("sse2"))) > > > buffer_zero_sse2(const void *buf, size_t len) > > > { > > > - __m128i t = _mm_loadu_si128(buf); > > > - __m128i *p = (__m128i *)(((uintptr_t)buf + 5 * 16) & -16); > > > - __m128i *e = (__m128i *)(((uintptr_t)buf + len) & -16); > > > - __m128i zero = _mm_setzero_si128(); > > > + /* Begin with an unaligned head and tail of 16 bytes. */ > > > + __m128i t = *(__m128i_u *)buf; > > > + __m128i t2 = *(__m128i_u *)(buf + len - 16); > > > + const __m128i *p = (void *)(((uintptr_t)buf + 16) & -16); > > > + const __m128i *e = (void *)(((uintptr_t)buf + len) & -16); > > > + __m128i zero = { 0 }; > > > > > > - /* Loop over 16-byte aligned blocks of 64. */ > > > - while (likely(p <= e)) { > > > - __builtin_prefetch(p); > > > + /* Proceed with an aligned tail. */ > > > + t2 |= e[-7]; > > > + t |= e[-6]; > > > + /* Use the barrier to ensure two independent chains. */ > > > + SSE_REASSOC_BARRIER(t, t2); > > > + t2 |= e[-5]; > > > + t |= e[-4]; > > > + SSE_REASSOC_BARRIER(t, t2); > > > + t2 |= e[-3]; > > > + t |= e[-2]; > > > + SSE_REASSOC_BARRIER(t, t2); > > > + t2 |= e[-1]; > > > + t |= t2; > > > + > > > + /* Loop over 16-byte aligned blocks of 128. */ > > > + while (likely(p < e - 7)) { > > > t = _mm_cmpeq_epi8(t, zero); > > > if (unlikely(_mm_movemask_epi8(t) != 0xFFFF)) { > > > return false; > > > } > > > - t = p[-4] | p[-3] | p[-2] | p[-1]; > > > - p += 4; > > > + t = p[0]; > > > + t2 = p[1]; > > > + SSE_REASSOC_BARRIER(t, t2); > > > + t |= p[2]; > > > + t2 |= p[3]; > > > + SSE_REASSOC_BARRIER(t, t2); > > > + t |= p[4]; > > > + t2 |= p[5]; > > > + SSE_REASSOC_BARRIER(t, t2); > > > + t |= p[6]; > > > + t2 |= p[7]; > > > + SSE_REASSOC_BARRIER(t, t2); > > > + t |= t2; > > > + p += 8; > > > } > > > > > > - /* Finish the aligned tail. */ > > > - t |= e[-3]; > > > - t |= e[-2]; > > > - t |= e[-1]; > > > - > > > - /* Finish the unaligned tail. */ > > > - t |= _mm_loadu_si128(buf + len - 16); > > > - > > > return _mm_movemask_epi8(_mm_cmpeq_epi8(t, zero)) == 0xFFFF; > > > } > > > > > > #ifdef CONFIG_AVX2_OPT > > > -static bool __attribute__((target("sse4"))) > > > -buffer_zero_sse4(const void *buf, size_t len) > > > -{ > > > - __m128i t = _mm_loadu_si128(buf); > > > - __m128i *p = (__m128i *)(((uintptr_t)buf + 5 * 16) & -16); > > > - __m128i *e = (__m128i *)(((uintptr_t)buf + len) & -16); > > > - > > > - /* Loop over 16-byte aligned blocks of 64. */ > > > - while (likely(p <= e)) { > > > - __builtin_prefetch(p); > > > - if (unlikely(!_mm_testz_si128(t, t))) { > > > - return false; > > > - } > > > - t = p[-4] | p[-3] | p[-2] | p[-1]; > > > - p += 4; > > > - } > > > - > > > - /* Finish the aligned tail. */ > > > - t |= e[-3]; > > > - t |= e[-2]; > > > - t |= e[-1]; > > > - > > > - /* Finish the unaligned tail. */ > > > - t |= _mm_loadu_si128(buf + len - 16); > > > - > > > - return _mm_testz_si128(t, t); > > > -} > > > > > > static bool __attribute__((target("avx2"))) > > > buffer_zero_avx2(const void *buf, size_t len) > > > { > > > /* Begin with an unaligned head of 32 bytes. */ > > > - __m256i t = _mm256_loadu_si256(buf); > > > - __m256i *p = (__m256i *)(((uintptr_t)buf + 5 * 32) & -32); > > > - __m256i *e = (__m256i *)(((uintptr_t)buf + len) & -32); > > > + __m256i t = *(__m256i_u *)buf; > > > + __m256i t2 = *(__m256i_u *)(buf + len - 32); > > > + const __m256i *p = (void *)(((uintptr_t)buf + 32) & -32); > > > + const __m256i *e = (void *)(((uintptr_t)buf + len) & -32); > > > + __m256i zero = { 0 }; > > > > > > - /* Loop over 32-byte aligned blocks of 128. */ > > > - while (p <= e) { > > > - __builtin_prefetch(p); > > > - if (unlikely(!_mm256_testz_si256(t, t))) { > > > + /* Proceed with an aligned tail. */ > > > + t2 |= e[-7]; > > > + t |= e[-6]; > > > + SSE_REASSOC_BARRIER(t, t2); > > > + t2 |= e[-5]; > > > + t |= e[-4]; > > > + SSE_REASSOC_BARRIER(t, t2); > > > + t2 |= e[-3]; > > > + t |= e[-2]; > > > + SSE_REASSOC_BARRIER(t, t2); > > > + t2 |= e[-1]; > > > + t |= t2; > > > + > > > + /* Loop over 32-byte aligned blocks of 256. */ > > > + while (likely(p < e - 7)) { > > > + t = _mm256_cmpeq_epi8(t, zero); > > > + if (unlikely(_mm256_movemask_epi8(t) != 0xFFFFFFFF)) { > > > return false; > > > } > > > - t = p[-4] | p[-3] | p[-2] | p[-1]; > > > - p += 4; > > > - } ; > > > + t = p[0]; > > > + t2 = p[1]; > > > + SSE_REASSOC_BARRIER(t, t2); > > > + t |= p[2]; > > > + t2 |= p[3]; > > > + SSE_REASSOC_BARRIER(t, t2); > > > + t |= p[4]; > > > + t2 |= p[5]; > > > + SSE_REASSOC_BARRIER(t, t2); > > > + t |= p[6]; > > > + t2 |= p[7]; > > > + SSE_REASSOC_BARRIER(t, t2); > > > + t |= t2; > > > + p += 8; > > > + } > > > > > > - /* Finish the last block of 128 unaligned. */ > > > - t |= _mm256_loadu_si256(buf + len - 4 * 32); > > > - t |= _mm256_loadu_si256(buf + len - 3 * 32); > > > - t |= _mm256_loadu_si256(buf + len - 2 * 32); > > > - t |= _mm256_loadu_si256(buf + len - 1 * 32); > > > - > > > - return _mm256_testz_si256(t, t); > > > + return _mm256_movemask_epi8(_mm256_cmpeq_epi8(t, zero)) == > > > 0xFFFFFFFF; > > > } > > > #endif /* CONFIG_AVX2_OPT */ > > > > > > -#ifdef CONFIG_AVX512F_OPT > > > -static bool __attribute__((target("avx512f"))) > > > -buffer_zero_avx512(const void *buf, size_t len) > > > -{ > > > - /* Begin with an unaligned head of 64 bytes. */ > > > - __m512i t = _mm512_loadu_si512(buf); > > > - __m512i *p = (__m512i *)(((uintptr_t)buf + 5 * 64) & -64); > > > - __m512i *e = (__m512i *)(((uintptr_t)buf + len) & -64); > > > - > > > - /* Loop over 64-byte aligned blocks of 256. */ > > > - while (p <= e) { > > > - __builtin_prefetch(p); > > > - if (unlikely(_mm512_test_epi64_mask(t, t))) { > > > - return false; > > > - } > > > - t = p[-4] | p[-3] | p[-2] | p[-1]; > > > - p += 4; > > > - } > > > - > > > - t |= _mm512_loadu_si512(buf + len - 4 * 64); > > > - t |= _mm512_loadu_si512(buf + len - 3 * 64); > > > - t |= _mm512_loadu_si512(buf + len - 2 * 64); > > > - t |= _mm512_loadu_si512(buf + len - 1 * 64); > > > - > > > - return !_mm512_test_epi64_mask(t, t); > > > - > > > -} > > > -#endif /* CONFIG_AVX512F_OPT */ > > > - > > > /* > > > * Make sure that these variables are appropriately initialized when > > > * SSE2 is enabled on the compiler command-line, but the compiler is > > > @@ -192,20 +172,17 @@ buffer_zero_avx512(const void *buf, size_t len) > > > */ > > > #if defined(CONFIG_AVX512F_OPT) || defined(CONFIG_AVX2_OPT) > > > # define INIT_USED 0 > > > -# define INIT_LENGTH 0 > > > -# define INIT_ACCEL buffer_zero_int > > > +# define INIT_ACCEL buffer_is_zero_len_4_plus > > > #else > > > # ifndef __SSE2__ > > > # error "ISA selection confusion" > > > # endif > > > # define INIT_USED CPUINFO_SSE2 > > > -# define INIT_LENGTH 64 > > > # define INIT_ACCEL buffer_zero_sse2 > > > #endif > > > > > > static unsigned used_accel = INIT_USED; > > > -static unsigned length_to_accel = INIT_LENGTH; > > > -static bool (*buffer_accel)(const void *, size_t) = INIT_ACCEL; > > > +bool (*buffer_is_zero_len_256_plus)(const void *, size_t) = INIT_ACCEL; > > > > > > static unsigned __attribute__((noinline)) > > > select_accel_cpuinfo(unsigned info) > > > @@ -213,24 +190,18 @@ select_accel_cpuinfo(unsigned info) > > > /* Array is sorted in order of algorithm preference. */ > > > static const struct { > > > unsigned bit; > > > - unsigned len; > > > bool (*fn)(const void *, size_t); > > > } all[] = { > > > -#ifdef CONFIG_AVX512F_OPT > > > - { CPUINFO_AVX512F, 256, buffer_zero_avx512 }, > > > -#endif > > > #ifdef CONFIG_AVX2_OPT > > > - { CPUINFO_AVX2, 128, buffer_zero_avx2 }, > > > - { CPUINFO_SSE4, 64, buffer_zero_sse4 }, > > > + { CPUINFO_AVX2, buffer_zero_avx2 }, > > > #endif > > > - { CPUINFO_SSE2, 64, buffer_zero_sse2 }, > > > - { CPUINFO_ALWAYS, 0, buffer_zero_int }, > > > + { CPUINFO_SSE2, buffer_zero_sse2 }, > > > + { CPUINFO_ALWAYS, buffer_is_zero_len_4_plus }, > > > }; > > > > > > for (unsigned i = 0; i < ARRAY_SIZE(all); ++i) { > > > if (info & all[i].bit) { > > > - length_to_accel = all[i].len; > > > - buffer_accel = all[i].fn; > > > + buffer_is_zero_len_256_plus = all[i].fn; > > > return all[i].bit; > > > } > > > } > > > @@ -256,35 +227,11 @@ bool test_buffer_is_zero_next_accel(void) > > > return used; > > > } > > > > > > -static bool select_accel_fn(const void *buf, size_t len) > > > -{ > > > - if (likely(len >= length_to_accel)) { > > > - return buffer_accel(buf, len); > > > - } > > > - return buffer_zero_int(buf, len); > > > -} > > > - > > > #else > > > -#define select_accel_fn buffer_zero_int > > > +#define select_accel_fn buffer_is_zero_len_4_plus > > > bool test_buffer_is_zero_next_accel(void) > > > { > > > return false; > > > } > > > #endif > > > > > > -/* > > > - * Checks if a buffer is all zeroes > > > - */ > > > -bool buffer_is_zero(const void *buf, size_t len) > > > -{ > > > - if (unlikely(len == 0)) { > > > - return true; > > > - } > > > - > > > - /* Fetch the beginning of the buffer while we select the > > > accelerator. */ > > > - __builtin_prefetch(buf); > > > - > > > - /* Use an optimized zero check if possible. Note that this also > > > - includes a check for an unrolled loop over 64-bit integers. */ > > > - return select_accel_fn(buf, len); > > > -} > > > > > >