Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
6e95843
resolve conflicts
supersonicbyte Aug 28, 2025
2ee5b64
linux fix
supersonicbyte Aug 27, 2025
5d45061
drop malloc small & large
supersonicbyte Aug 28, 2025
d17af85
resolve
supersonicbyte Aug 28, 2025
b1912c4
fix linux env path
supersonicbyte Aug 28, 2025
ba18ccd
add comment
supersonicbyte Aug 28, 2025
3a3e0f2
rebase with main
supersonicbyte Aug 28, 2025
8372094
remove dynamic test
supersonicbyte Aug 28, 2025
95c613c
run swift format
supersonicbyte Aug 28, 2025
1dc55b4
remove test target
supersonicbyte Aug 28, 2025
7aa8368
fix tests
supersonicbyte Aug 28, 2025
675f682
fix swift lint
supersonicbyte Aug 28, 2025
6858ed0
resolve pr comments
supersonicbyte Sep 1, 2025
2bbc2ad
resolve pr comments
supersonicbyte Sep 1, 2025
87e6ec1
fixes for malloc interposer
supersonicbyte Sep 2, 2025
59deb29
fix malloc size linux
supersonicbyte Sep 2, 2025
35924ef
Merge branch 'main' into feature/sc-23696/replace-jemalloc-dependency…
hassila Nov 3, 2025
c884d7b
run swift-format
supersonicbyte Dec 11, 2025
a9edbb8
merge main
supersonicbyte Mar 5, 2026
46e7224
improve malloc interposer performance
supersonicbyte Mar 6, 2026
0eeb00a
delete Package.resolved
supersonicbyte Mar 6, 2026
73f19c7
add missing cases
supersonicbyte Mar 10, 2026
56828a7
add preconcurrency on import
supersonicbyte Mar 10, 2026
5767bec
add benchmarks for MallocInterposer
supersonicbyte May 7, 2026
0232374
add malloc and jemalloc comparison
supersonicbyte May 8, 2026
94543e3
store requested size in custom header prefix instead of calling mallo…
supersonicbyte May 8, 2026
c93c8a8
Merge branch 'main' into feature/sc-23696/replace-jemalloc-dependency…
supersonicbyte May 8, 2026
1944497
fix linux
supersonicbyte May 8, 2026
92b714f
update compare script
supersonicbyte May 8, 2026
876e71f
merge main
supersonicbyte May 11, 2026
2a3efb3
move interposer to seperate package
supersonicbyte May 14, 2026
f1ac619
Merge branch 'main' into feature/sc-23696/replace-jemalloc-dependency…
supersonicbyte May 29, 2026
dedd0e3
rename to benchmark
supersonicbyte May 29, 2026
3686bc1
add overhead scripts
supersonicbyte Jun 1, 2026
a4989f6
update overhead script
supersonicbyte Jun 1, 2026
1931a4f
linux fix
supersonicbyte Jun 1, 2026
e227d8e
fix
supersonicbyte Jun 16, 2026
5013c52
Merge branch 'main' into feature/sc-23696/replace-jemalloc-dependency…
hassila Jun 16, 2026
553017e
add new metric
supersonicbyte Jun 16, 2026
0b9aed4
fix swiftlint issues
supersonicbyte Jun 16, 2026
e394b27
fix version
supersonicbyte Jun 16, 2026
f5f5634
leave mallocSmall and mallocLarge metrics
supersonicbyte Jun 16, 2026
35e81f3
remove jemalloc trait form swift 6.3 manifest
supersonicbyte Jun 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore


.DS_Store

## User settings
xcuserdata/

Expand Down Expand Up @@ -57,6 +60,7 @@ Package.resolved
.swiftpm
.DS_Store
.build/
.build-*/

# CocoaPods
#
Expand Down
202 changes: 202 additions & 0 deletions Benchmarks/Benchmarks/MallocInterposer/MallocInterposer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
//
// Copyright (c) 2026 Ordo One AB
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Regression benchmarks for the malloc interposer. Each benchmark performs
// a known, fixed number of allocations per iteration so the reported
// per-iteration counts (mallocCountTotal / freeCountTotal / etc.) line up
// with the expected values noted in the benchmark name. Drift between the
// jemalloc and interposer code paths — or between branches — shows up
// immediately as a count mismatch.
//
// Counts are scaled per iteration: with .kilo scaling, one malloc inside
// the body produces "1" in the count column, not "1000".

import Benchmark

#if canImport(Darwin)
import Darwin
#elseif canImport(Glibc)
import Glibc
#elseif canImport(Musl)
import Musl
#else
#error("Unsupported Platform")
#endif

let mallocMetrics: [BenchmarkMetric] = [
.wallClock,
.mallocCountSmall,
.mallocCountLarge,
.mallocCountTotal,
.freeCountTotal,
.mallocBytesCount,
.mallocFreeDelta,
.memoryLeakedBytes,
]

let benchmarks: @Sendable () -> Void = {
Benchmark.defaultConfiguration = .init(
metrics: mallocMetrics,
warmupIterations: 1,
scalingFactor: .kilo,
maxDuration: .seconds(1),
maxIterations: 100
)

// Sanity floor: an empty body should report (close to) zero allocations.
// Whatever the framework's per-iteration overhead is, it shows up here
// and is the reference for what "no allocations" looks like.
Benchmark("Noop") { benchmark in
for _ in benchmark.scaledIterations {
blackHole(0)
}
}

// Bread-and-butter malloc/free pair, sub-page size — should land in
// mallocCountSmall, not mallocCountLarge.
// Expected per iter: malloc=1 (small=1, large=0), free=1, leaked=0.
Benchmark("Malloc 64B + free") { benchmark in
for _ in benchmark.scaledIterations {
let ptr = malloc(64)
blackHole(ptr)
free(ptr)
}
}

// Larger-than-page allocation — should land in mallocCountLarge.
// Expected per iter: malloc=1 (small=0, large=1), free=1.
Benchmark("Malloc 2 MiB + free") { benchmark in
for _ in benchmark.scaledIterations {
let ptr = malloc(2 * 1_024 * 1_024)
blackHole(ptr)
free(ptr)
}
}

// calloc must be counted exactly like malloc + memset.
// Expected per iter: malloc=1, free=1.
Benchmark("Calloc 8x8 + free") { benchmark in
for _ in benchmark.scaledIterations {
let ptr = calloc(8, 8)
blackHole(ptr)
free(ptr)
}
}

// realloc(grow) on success: implicit free of old + alloc of new.
// Expected per iter: malloc=2, free=2.
Benchmark("Realloc grow 64→256 + free") { benchmark in
for _ in benchmark.scaledIterations {
let original = malloc(64)
let grown = realloc(original, 256)
blackHole(grown)
free(grown)
}
}

// realloc(NULL, size) is a pure malloc — no implicit free.
// Expected per iter: malloc=1, free=1.
Benchmark("Realloc(NULL, 128) + free") { benchmark in
for _ in benchmark.scaledIterations {
let ptr = realloc(nil, 128)
blackHole(ptr)
free(ptr)
}
}

// realloc(p, 0) frees p and returns NULL — pure free, no second malloc.
// Expected per iter: malloc=1, free=1.
Benchmark("Malloc + realloc(p, 0)") { benchmark in
for _ in benchmark.scaledIterations {
let ptr = malloc(64)
let resized = realloc(ptr, 0)
blackHole(resized) // expected nil
}
}

// posix_memalign — separate code path that's easy to forget to count.
// Expected per iter: malloc=1, free=1.
Benchmark("posix_memalign(64, 1024) + free") { benchmark in
var ptr: UnsafeMutableRawPointer?
for _ in benchmark.scaledIterations {
_ = posix_memalign(&ptr, 64, 1_024)
blackHole(ptr)
free(ptr)
}
}

// C11 aligned_alloc — currently only intercepted on Linux. On Darwin the
// count drops because the symbol isn't in the DYLD_INTERPOSE list. Useful
// signal for that gap.
// Expected per iter (Linux): malloc=1, free=1.
// Expected per iter (Darwin): malloc=0 (not interposed), free=1.
#if !canImport(Darwin)
Benchmark("aligned_alloc(64, 1024) + free") { benchmark in
for _ in benchmark.scaledIterations {
let ptr = aligned_alloc(64, 1_024)
blackHole(ptr)
free(ptr)
}
}
#endif

// Batched mallocs in a single iteration — verifies the counter scales
// linearly and isn't accidentally collapsed/de-duplicated.
// Expected per iter: malloc=16, free=16.
Benchmark("Malloc x16 + free x16") { benchmark in
let count = 16
let buf = UnsafeMutablePointer<UnsafeMutableRawPointer?>.allocate(capacity: count)
defer { buf.deallocate() }
buf.update(repeating: nil, count: count)

for _ in benchmark.scaledIterations {
for i in 0..<count {
buf[i] = malloc(48)
}
for i in 0..<count {
free(buf[i])
}
}
}

// Deliberate leak: malloc without free. Confirms mallocFreeDelta /
// memoryLeakedBytes track unbalanced flow correctly.
// Expected per iter: malloc=1, free=0, leaked=1, leakedBytes≈128.
// The accumulated leak across the run is bounded:
// <= maxIterations * scalingFactor * 128 = 100 * 1000 * 128 = ~12.5 MiB.
Benchmark("Leak: malloc 128B (no free)") { benchmark in
for _ in benchmark.scaledIterations {
let ptr = malloc(128)
blackHole(ptr)
}
}

// Swift stdlib path: Array(repeating:count:) goes through swift_allocObject
// which (on supported platforms) lowers to malloc. The exact count per
// iter depends on stdlib internals, but it must be > 0 and stable
// between runs.
Benchmark("Swift Array<Int>(repeating:0, count:128)") { benchmark in
for _ in benchmark.scaledIterations {
var arr = [Int](repeating: 0, count: 128)
arr.withUnsafeMutableBufferPointer { buf in
blackHole(buf.baseAddress)
}
}
}

// Heap-allocated String (must exceed the small-string inline limit of
// 15 bytes). Same caveat as Array — count is stdlib-dependent but must
// be stable.
Benchmark("Swift String (long, heap)") { benchmark in
for _ in benchmark.scaledIterations {
let str = String(repeating: "x", count: 256)
blackHole(str)
}
}
}
22 changes: 11 additions & 11 deletions Benchmarks/Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions Benchmarks/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,20 @@ package.targets += [
]
)
]

// Regression coverage for the malloc interposer: predictable allocation
// patterns (counts known per iteration) so any drift between jemalloc and
// interposer code paths is immediately visible in mallocCountTotal /
// freeCountTotal / mallocFreeDelta / memoryLeakedBytes.
package.targets += [
.executableTarget(
name: "MallocInterposerBenchmarks",
dependencies: [
.product(name: "Benchmark", package: "benchmark")
],
path: "Benchmarks/MallocInterposer",
plugins: [
.plugin(name: "BenchmarkPlugin", package: "benchmark")
]
)
]
48 changes: 28 additions & 20 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,32 +1,36 @@
// swift-tools-version: 6.1
// swift-tools-version: 6.3

import PackageDescription

import class Foundation.ProcessInfo

// If the environment variable BENCHMARK_DISABLE_JEMALLOC is set disable Jemalloc trait (backward compatibility)
let disableJemalloc = ProcessInfo.processInfo.environment["BENCHMARK_DISABLE_JEMALLOC"] != nil

let defaultTraits: Set<String>

if disableJemalloc {
defaultTraits = []
} else {
defaultTraits = ["Jemalloc"]
}
// When MALLOC_INTERPOSER_LOCAL_PATH is set, use a local checkout of the
// malloc-interposer package instead of the published GitHub URL. Useful
// when iterating on the interposer alongside this package.
let mallocInterposerDependency: Package.Dependency = {
if let localPath = ProcessInfo.processInfo.environment["MALLOC_INTERPOSER_LOCAL_PATH"],
localPath.isEmpty == false
{
return .package(path: localPath)
}
return .package(
url: "https://github.com/ordo-one/malloc-interposer.git",
.upToNextMajor(from: "1.0.0")
)
}()

var packageDependencies: [Package.Dependency] = [
.package(url: "https://github.com/apple/swift-system.git", .upToNextMajor(from: "1.1.0")),
.package(url: "https://github.com/apple/swift-argument-parser.git", .upToNextMajor(from: "1.6.0")),
.package(url: "https://github.com/ordo-one/TextTable.git", .upToNextMajor(from: "0.0.1")),
.package(url: "https://github.com/HdrHistogram/hdrhistogram-swift.git", .upToNextMajor(from: "0.1.4")),
.package(url: "https://github.com/apple/swift-atomics.git", .upToNextMajor(from: "1.0.0")),
.package(url: "https://github.com/ordo-one/package-jemalloc.git", .upToNextMajor(from: "1.0.0")),
mallocInterposerDependency,
]

#if os(Linux) && compiler(>=6.3)
packageDependencies += [
.package(url: "https://github.com/ordo-one/swift-runtime-interposer.git", .upToNextMajor(from: "1.0.0")),
.package(url: "https://github.com/ordo-one/swift-runtime-interposer.git", .upToNextMajor(from: "1.0.0"))
]
#endif

Expand All @@ -39,13 +43,21 @@ var benchmarkDependencies: [Target.Dependency] = [
.product(name: "Atomics", package: "swift-atomics"),
"SwiftRuntimeHooks",
"BenchmarkShared",
.product(name: "jemalloc", package: "package-jemalloc", condition: .when(platforms: [.macOS, .linux], traits: ["Jemalloc"])),
.product(name: "MallocInterposerSwift", package: "malloc-interposer"),
]

#if os(Linux) && compiler(>=6.3)
benchmarkDependencies += [
.product(name: "SwiftRuntimeInterposerC", package: "swift-runtime-interposer", condition: .when(platforms: [.linux])),
.product(name: "SwiftRuntimeInterposerSwift", package: "swift-runtime-interposer", condition: .when(platforms: [.linux])),
.product(
name: "SwiftRuntimeInterposerC",
package: "swift-runtime-interposer",
condition: .when(platforms: [.linux])
),
.product(
name: "SwiftRuntimeInterposerSwift",
package: "swift-runtime-interposer",
condition: .when(platforms: [.linux])
),
]
#endif

Expand All @@ -63,10 +75,6 @@ let package = Package(
targets: ["Benchmark"]
),
],
traits: [
.trait(name: "Jemalloc"),
.default(enabledTraits: defaultTraits),
],
dependencies: packageDependencies,
targets: [
.target(
Expand Down
Loading
Loading