Skip to content

[libc] Always use __builtin_wasm_memory_fill in memset - #27656

Merged
sbc100 merged 1 commit into
emscripten-core:mainfrom
sbc100:always-bulkmem-memset
Sep 4, 2026
Merged

[libc] Always use __builtin_wasm_memory_fill in memset#27656
sbc100 merged 1 commit into
emscripten-core:mainfrom
sbc100:always-bulkmem-memset

Conversation

@sbc100

@sbc100 sbc100 commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Followup to #27653 and #27656.

Replace Musl's memset.c and the -Oz logic in emscripten_memset.c with __builtin_wasm_memory_fill.

Under ASan, keep __attribute__((no_sanitize("address"))) on the naive scalar loop because ASan's shadow memory poisoner calls REAL(memset) directly on shadow memory itself, which would otherwise trigger an immediate shadow-on-shadow fault.

Microbenchmark results (100k iterations across sizes 1B to 16KB):

Aligned sets (sizes 1B to 16KB):

  • V8: 30.36ms vs 70.52ms (56.9% faster, 2.3x)
  • SpiderMonkey: 31.91ms vs 67.01ms (52.4% faster, 2.1x)
  • JavaScriptCore: 26.70ms vs 70.44ms (62.1% faster, 2.6x)

Unaligned sets (offsets 1..3 across sizes 1B to 16KB):

  • V8: 30.61ms vs 65.28ms (53.1% faster, 2.1x)
  • SpiderMonkey: 32.94ms vs 64.28ms (48.8% faster, 2.0x)
  • JavaScriptCore: 26.90ms vs 63.24ms (57.5% faster, 2.4x)

sbc100 added a commit to sbc100/emscripten that referenced this pull request Sep 2, 2026
Followup to emscripten-core#27653 and emscripten-core#27656.

Replace Musl's `memset.c` and the `-Oz` logic in `emscripten_memset.c`
with `__builtin_wasm_memory_fill`.

Under ASan, keep `__attribute__((no_sanitize("address")))` on the naive
scalar loop because ASan's shadow memory poisoner calls `REAL(memset)`
directly on shadow memory itself, which would otherwise trigger an
immediate shadow-on-shadow fault.

Microbenchmark results (100k iterations across sizes 1B to 16KB):

Aligned sets (sizes 1B to 16KB):
- V8:             30.36ms vs 70.52ms (56.9% faster, 2.3x)
- SpiderMonkey:   31.91ms vs 67.01ms (52.4% faster, 2.1x)
- JavaScriptCore: 26.70ms vs 70.44ms (62.1% faster, 2.6x)

Unaligned sets (offsets 1..3 across sizes 1B to 16KB):
- V8:             30.61ms vs 65.28ms (53.1% faster, 2.1x)
- SpiderMonkey:   32.94ms vs 64.28ms (48.8% faster, 2.0x)
- JavaScriptCore: 26.90ms vs 63.24ms (57.5% faster, 2.4x)
@sbc100
sbc100 force-pushed the always-bulkmem-memset branch from 5821f95 to b9bbec3 Compare September 2, 2026 23:16
@sbc100
sbc100 requested a review from dschuff September 2, 2026 23:19
sbc100 added a commit to sbc100/emscripten that referenced this pull request Sep 2, 2026
Followup to emscripten-core#27653 and emscripten-core#27656.

Replace Musl's `memset.c` and the `-Oz` logic in `emscripten_memset.c`
with `__builtin_wasm_memory_fill`.

Under ASan, keep `__attribute__((no_sanitize("address")))` on the naive
scalar loop because ASan's shadow memory poisoner calls `REAL(memset)`
directly on shadow memory itself, which would otherwise trigger an
immediate shadow-on-shadow fault.

Microbenchmark results (100k iterations across sizes 1B to 16KB):

Aligned sets (sizes 1B to 16KB):
- V8:             30.36ms vs 70.52ms (56.9% faster, 2.3x)
- SpiderMonkey:   31.91ms vs 67.01ms (52.4% faster, 2.1x)
- JavaScriptCore: 26.70ms vs 70.44ms (62.1% faster, 2.6x)

Unaligned sets (offsets 1..3 across sizes 1B to 16KB):
- V8:             30.61ms vs 65.28ms (53.1% faster, 2.1x)
- SpiderMonkey:   32.94ms vs 64.28ms (48.8% faster, 2.0x)
- JavaScriptCore: 26.90ms vs 63.24ms (57.5% faster, 2.4x)
@sbc100
sbc100 force-pushed the always-bulkmem-memset branch from b9bbec3 to 28cc785 Compare September 2, 2026 23:21
sbc100 added a commit to sbc100/emscripten that referenced this pull request Sep 3, 2026
Followup to emscripten-core#27653 and emscripten-core#27656.

Replace Musl's `memset.c` and the `-Oz` logic in `emscripten_memset.c`
with `__builtin_wasm_memory_fill`.

Under ASan, keep `__attribute__((no_sanitize("address")))` on the naive
scalar loop because ASan's shadow memory poisoner calls `REAL(memset)`
directly on shadow memory itself, which would otherwise trigger an
immediate shadow-on-shadow fault.

Microbenchmark results (100k iterations across sizes 1B to 16KB):

Aligned sets (sizes 1B to 16KB):
- V8:             30.36ms vs 70.52ms (56.9% faster, 2.3x)
- SpiderMonkey:   31.91ms vs 67.01ms (52.4% faster, 2.1x)
- JavaScriptCore: 26.70ms vs 70.44ms (62.1% faster, 2.6x)

Unaligned sets (offsets 1..3 across sizes 1B to 16KB):
- V8:             30.61ms vs 65.28ms (53.1% faster, 2.1x)
- SpiderMonkey:   32.94ms vs 64.28ms (48.8% faster, 2.0x)
- JavaScriptCore: 26.90ms vs 63.24ms (57.5% faster, 2.4x)
@sbc100
sbc100 force-pushed the always-bulkmem-memset branch from 28cc785 to cc09756 Compare September 3, 2026 00:15
@sbc100
sbc100 force-pushed the always-bulkmem-memmove branch from 0ebd33c to d18ee60 Compare September 3, 2026 00:17
@kripken

kripken commented Sep 3, 2026

Copy link
Copy Markdown
Member

I would once again like to see the benchmark, if it's not too much trouble...

@sbc100
sbc100 deleted the branch emscripten-core:main September 3, 2026 17:47
@sbc100 sbc100 closed this Sep 3, 2026
Followup to emscripten-core#27653 and emscripten-core#27656.

Replace Musl's `memset.c` and the `-Oz` logic in `emscripten_memset.c`
with `__builtin_wasm_memory_fill`.

Under ASan, keep `__attribute__((no_sanitize("address")))` on the naive
scalar loop because ASan's shadow memory poisoner calls `REAL(memset)`
directly on shadow memory itself, which would otherwise trigger an
immediate shadow-on-shadow fault.

Microbenchmark results (100k iterations across sizes 1B to 16KB):

Aligned sets (sizes 1B to 16KB):
- V8:             30.36ms vs 70.52ms (56.9% faster, 2.3x)
- SpiderMonkey:   31.91ms vs 67.01ms (52.4% faster, 2.1x)
- JavaScriptCore: 26.70ms vs 70.44ms (62.1% faster, 2.6x)

Unaligned sets (offsets 1..3 across sizes 1B to 16KB):
- V8:             30.61ms vs 65.28ms (53.1% faster, 2.1x)
- SpiderMonkey:   32.94ms vs 64.28ms (48.8% faster, 2.0x)
- JavaScriptCore: 26.90ms vs 63.24ms (57.5% faster, 2.4x)
@sbc100

sbc100 commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator Author

AI-Generated: memset Benchmark Details & Workloads

Here is the microbenchmark used to measure __builtin_wasm_memory_fill vs Musl's memset implementation for aligned and unaligned sets:

Source Code (compare_memset.c)

#include <stdint.h>
#include <string.h>
#include <stdio.h>
#include <emscripten/emscripten.h>

char dst[1024*1024*16+16] = {};
volatile uint8_t checksum = 0;

// Musl's memset implementation
void *__attribute__((noinline)) memset_musl(void *dest, int c, size_t n)
{
	unsigned char *s = (unsigned char *)dest;
	size_t k;

	if (!n) return dest;
	s[0] = c;
	s[n-1] = c;
	if (n <= 2) return dest;
	s[1] = c;
	s[2] = c;
	s[n-2] = c;
	s[n-3] = c;
	if (n <= 6) return dest;
	s[3] = c;
	s[n-4] = c;
	if (n <= 8) return dest;

	k = -(uintptr_t)s & 3;
	s += k;
	n -= k;
	n &= -4;

	typedef uint32_t __attribute__((__may_alias__)) u32;
	typedef uint64_t __attribute__((__may_alias__)) u64;

	u32 c32 = ((u32)-1)/255 * (unsigned char)c;

	*(u32 *)(s+0) = c32;
	*(u32 *)(s+n-4) = c32;
	if (n <= 8) return dest;
	*(u32 *)(s+4) = c32;
	*(u32 *)(s+8) = c32;
	*(u32 *)(s+n-12) = c32;
	*(u32 *)(s+n-8) = c32;
	if (n <= 24) return dest;
	*(u32 *)(s+12) = c32;
	*(u32 *)(s+16) = c32;
	*(u32 *)(s+20) = c32;
	*(u32 *)(s+24) = c32;
	*(u32 *)(s+n-28) = c32;
	*(u32 *)(s+n-24) = c32;
	*(u32 *)(s+n-20) = c32;
	*(u32 *)(s+n-16) = c32;

	k = 24 + ((uintptr_t)s & 4);
	s += k;
	n -= k;

	u64 c64 = c32 | ((u64)c32 << 32);
	for (; n >= 32; n-=32, s+=32) {
		*(u64 *)(s+0) = c64;
		*(u64 *)(s+8) = c64;
		*(u64 *)(s+16) = c64;
		*(u64 *)(s+24) = c64;
	}

	return dest;
}

// Builtin memory.fill implementation
void *__attribute__((noinline)) memset_bulkmem(void *str, int c, size_t n) {
  if (n) {
    __builtin_wasm_memory_fill(0, str, c, n);
  }
  return str;
}

int sizes[] = {1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 4096, 16384};
int num_sizes = sizeof(sizes)/sizeof(sizes[0]);

int main() {
  const int iters = 100000;

  // Aligned
  double t0 = emscripten_get_now();
  for (int s = 0; s < num_sizes; s++) {
    int sz = sizes[s];
    for (int i = 0; i < iters; i++) {
      memset_musl(dst, i, sz);
      checksum += dst[sz >> 1];
    }
  }
  double t_old = emscripten_get_now() - t0;

  t0 = emscripten_get_now();
  for (int s = 0; s < num_sizes; s++) {
    int sz = sizes[s];
    for (int i = 0; i < iters; i++) {
      memset_bulkmem(dst, i, sz);
      checksum += dst[sz >> 1];
    }
  }
  double t_new = emscripten_get_now() - t0;

  printf("Aligned (1B..16KB, %d iters):\n", iters);
  printf("  Musl memset:     %6.2f ms\n", t_old);
  printf("  bulkmem fill:    %6.2f ms\n", t_new);
  printf("  Speedup:         %5.1f%% %s\n", (1.0 - t_new/t_old)*100.0, t_new <= t_old ? "faster" : "slower");

  // Unaligned (offsets 1, 2, 3)
  int offsets[] = {1, 2, 3};
  t0 = emscripten_get_now();
  for (int o = 0; o < 3; o++) {
    char *d = dst + offsets[o];
    for (int s = 0; s < num_sizes; s++) {
      int sz = sizes[s];
      for (int i = 0; i < iters/3; i++) {
        memset_musl(d, i, sz);
        checksum += d[sz >> 1];
      }
    }
  }
  double t_old_unaligned = emscripten_get_now() - t0;

  t0 = emscripten_get_now();
  for (int o = 0; o < 3; o++) {
    char *d = dst + offsets[o];
    for (int s = 0; s < num_sizes; s++) {
      int sz = sizes[s];
      for (int i = 0; i < iters/3; i++) {
        memset_bulkmem(d, i, sz);
        checksum += d[sz >> 1];
      }
    }
  }
  double t_new_unaligned = emscripten_get_now() - t0;

  printf("Unaligned (1B..16KB, %d iters across offsets 1,2,3):\n", iters);
  printf("  Musl memset:     %6.2f ms\n", t_old_unaligned);
  printf("  bulkmem fill:    %6.2f ms\n", t_new_unaligned);
  printf("  Speedup:         %5.1f%% %s\n", (1.0 - t_new_unaligned/t_old_unaligned)*100.0, t_new_unaligned <= t_old_unaligned ? "faster" : "slower");

  return 0;
}

Compilation & Runner

emcc -O3 compare_memset.c -o compare_memset.js -sINITIAL_MEMORY=64MB -sENVIRONMENT=node,shell

# Engines:
~/.jsvu/bin/v8 compare_memset.js
~/.jsvu/bin/spidermonkey compare_memset.js
~/.jsvu/bin/javascriptcore compare_memset.js

@sbc100 sbc100 reopened this Sep 3, 2026
@sbc100
sbc100 changed the base branch from always-bulkmem-memmove to main September 3, 2026 17:58
@sbc100
sbc100 force-pushed the always-bulkmem-memset branch from cc09756 to a36be01 Compare September 3, 2026 17:58
@sbc100
sbc100 requested review from dschuff and kripken September 3, 2026 19:21
@kripken

kripken commented Sep 4, 2026

Copy link
Copy Markdown
Member

Thanks!

Interesting, inlining does not matter here. I guess this is pretty optimal regardless.

@sbc100
sbc100 merged commit f0cd673 into emscripten-core:main Sep 4, 2026
42 checks passed
@sbc100
sbc100 deleted the always-bulkmem-memset branch September 4, 2026 16:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants