Skip to content

Commit 81266e1

Browse files
authored
Merge pull request #291 from plasma-umass/interpose-semaphores
Interpose semaphores, and don't let delay accounting underflow duration
2 parents 56ac187 + 1eb5a37 commit 81266e1

11 files changed

Lines changed: 462 additions & 5 deletions

File tree

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
#!/usr/bin/env python3
2+
"""Assert that coz measures a real speedup for benchmarks/sem_toy.
3+
4+
sem_toy's main thread joins its workers on a semaphore. A thread blocked on a
5+
semaphore is not running, so it must not be charged for the virtual delays
6+
inserted while it slept. When libcoz does not interpose the semaphore, the main
7+
thread -- the one that visits the progress point -- pays them all on wake-up,
8+
the measured period grows with the speedup, and the slope collapses to zero or
9+
goes negative.
10+
11+
Both of sem_toy's loops inline the same xorshift, so the hot line's slope should
12+
approach 1.0: removing that work removes the program.
13+
"""
14+
import json
15+
import sys
16+
17+
18+
def main() -> int:
19+
if len(sys.argv) != 2:
20+
print(f"usage: {sys.argv[0]} <profile.jsonl>")
21+
return 2
22+
23+
rows, current = [], None
24+
with open(sys.argv[1]) as handle:
25+
for line in handle:
26+
line = line.strip()
27+
if not line:
28+
continue
29+
record = json.loads(line)
30+
if record["type"] == "experiment":
31+
current = record
32+
elif record["type"] == "throughput-point" and current is not None:
33+
current["delta"] = record["delta"]
34+
rows.append(current)
35+
current = None
36+
37+
if not rows:
38+
print("ERROR: no experiments in profile")
39+
return 1
40+
41+
# A duration past 2^63 means the inserted delay exceeded the experiment's
42+
# wall time and the unsigned subtraction wrapped.
43+
underflowed = [r for r in rows if r["duration"] > 10**12]
44+
if underflowed:
45+
print(f"ERROR: {len(underflowed)} experiment(s) with underflowed duration")
46+
return 1
47+
48+
periods = {}
49+
for row in rows:
50+
if row["delta"]:
51+
periods.setdefault(round(row["speedup"], 2), []).append(row["duration"] / row["delta"])
52+
53+
baseline = periods.get(0.0)
54+
if not baseline:
55+
print("WARNING: no 0% baseline experiments; skipping slope check")
56+
return 0
57+
58+
best = max(periods)
59+
if best < 0.5:
60+
print(f"WARNING: largest speedup sampled was {best:.0%}; skipping slope check")
61+
return 0
62+
63+
p0 = sum(baseline) / len(baseline)
64+
ps = sum(periods[best]) / len(periods[best])
65+
improvement = 1 - ps / p0
66+
67+
print(f"baseline period {p0:.0f}ns; period at {best:.0%} speedup {ps:.0f}ns")
68+
print(f"measured improvement: {improvement:.1%}")
69+
70+
if improvement < 0.25:
71+
print("ERROR: virtual speedup produced no throughput gain -- is the "
72+
"blocked main thread being charged for delays it never paid?")
73+
return 1
74+
75+
print("Semaphore interposition validated")
76+
return 0
77+
78+
79+
if __name__ == "__main__":
80+
sys.exit(main())

.github/workflows/ci.yml

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,15 @@ jobs:
4242
run: |
4343
cd build
4444
cmake .. -DBUILD_BENCHMARKS=ON -DCMAKE_BUILD_TYPE=RelWithDebInfo
45-
make -j$(nproc) toy lock_test kmeans
45+
make -j$(nproc) toy lock_test kmeans sem_toy
4646
4747
- name: Verify build artifacts
4848
run: |
4949
test -f build/libcoz/libcoz.so
5050
test -x build/benchmarks/toy/toy
5151
test -x build/benchmarks/lock_test/lock_test
5252
test -x build/benchmarks/kmeans/kmeans
53+
test -x build/benchmarks/sem_toy/sem_toy
5354
echo "Build artifacts verified"
5455
5556
- name: Run unit tests
@@ -232,6 +233,20 @@ jobs:
232233
print(" - lock_test.cpp:12 (critical section) identified as profiling target")
233234
VALIDATION_SCRIPT
234235
236+
237+
- name: Run sem_toy (semaphore join) with coz
238+
run: |
239+
cd build
240+
TIMEOUT=timeout
241+
command -v gtimeout >/dev/null && TIMEOUT=gtimeout
242+
$TIMEOUT 120 ../coz run -o sem_toy_profile.jsonl --- ./benchmarks/sem_toy/sem_toy || true
243+
test -s sem_toy_profile.jsonl
244+
245+
- name: Validate semaphore interposition
246+
run: |
247+
cd build
248+
python3 ../.github/scripts/check_semaphore_speedup.py sem_toy_profile.jsonl
249+
235250
- name: Upload profile artifacts
236251
uses: actions/upload-artifact@v4
237252
with:
@@ -265,7 +280,7 @@ jobs:
265280
run: |
266281
cd build
267282
cmake .. -DBUILD_BENCHMARKS=ON -DCMAKE_BUILD_TYPE=RelWithDebInfo
268-
make -j$(sysctl -n hw.ncpu) toy lock_test kmeans
283+
make -j$(sysctl -n hw.ncpu) toy lock_test kmeans sem_toy
269284
270285
- name: Verify build artifacts
271286
run: |
@@ -275,6 +290,7 @@ jobs:
275290
test -x build/benchmarks/toy/toy
276291
test -x build/benchmarks/lock_test/lock_test
277292
test -x build/benchmarks/kmeans/kmeans
293+
test -x build/benchmarks/sem_toy/sem_toy
278294
echo "Build artifacts verified"
279295
280296
- name: Run unit tests
@@ -296,6 +312,17 @@ jobs:
296312
echo "Profile created:"
297313
cat profile.jsonl
298314
315+
- name: Run sem_toy (semaphore join) with coz
316+
run: |
317+
cd build
318+
gtimeout 120 ../coz run -o sem_toy_profile.jsonl --- ./benchmarks/sem_toy/sem_toy || true
319+
test -s sem_toy_profile.jsonl
320+
321+
- name: Validate semaphore interposition
322+
run: |
323+
cd build
324+
python3 ../.github/scripts/check_semaphore_speedup.py sem_toy_profile.jsonl
325+
299326
lang-rust:
300327
name: Rust bindings (${{ matrix.os }})
301328
runs-on: ${{ matrix.os }}

benchmarks/sem_toy/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
add_executable(sem_toy sem_toy.cpp)
2+
target_link_libraries(sem_toy PRIVATE pthread coz-instrumentation)
3+
4+
add_coz_run_target(run_sem_toy COMMAND $<TARGET_FILE:sem_toy>)

benchmarks/sem_toy/sem_toy.cpp

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
* Copyright (c) 2015, Charlie Curtsinger and Emery Berger,
3+
* University of Massachusetts Amherst
4+
* This file is part of the Coz project. See LICENSE.md file at the top-level
5+
* directory of this distribution and at http://github.com/plasma-umass/coz.
6+
*/
7+
8+
// Like benchmarks/toy, but the main thread joins its workers through a
9+
// semaphore rather than pthread_join.
10+
//
11+
// This is the regression test for semaphore interposition. A thread blocked on
12+
// a semaphore is not running, so it must not be charged for virtual delays
13+
// inserted while it slept. Before libcoz wrapped sem_wait/semaphore_wait, the
14+
// main thread -- the one that visits the progress point -- paid all of them on
15+
// wake-up, and the profile came out with a slope near zero or negative
16+
// (measured: +0.13 with R^2 0.01, and -0.57 with R^2 0.18) instead of the ~1.0
17+
// this program should show.
18+
//
19+
// Both loops inline the same xorshift, so the expected result is a single hot
20+
// line with a slope near 1.0: removing that work removes the program.
21+
#include <coz.h>
22+
#include <pthread.h>
23+
#include <stdio.h>
24+
#include <stdint.h>
25+
26+
#ifdef __APPLE__
27+
#include <dispatch/dispatch.h>
28+
static dispatch_semaphore_t done;
29+
static void sem_setup() { done = dispatch_semaphore_create(0); }
30+
static void sem_wait_one() { dispatch_semaphore_wait(done, DISPATCH_TIME_FOREVER); }
31+
static void sem_signal() { dispatch_semaphore_signal(done); }
32+
#else
33+
#include <semaphore.h>
34+
static sem_t done;
35+
static void sem_setup() { sem_init(&done, 0, 0); }
36+
static void sem_wait_one() { sem_wait(&done); }
37+
static void sem_signal() { sem_post(&done); }
38+
#endif
39+
40+
static const uint64_t kIterations = 40000000ULL;
41+
static volatile uint64_t slow_sink, fast_sink;
42+
43+
static uint64_t xorshift(uint64_t v) {
44+
v ^= v << 13; v ^= v >> 7; v ^= v << 17; return v;
45+
}
46+
47+
static void* slow_work(void*) {
48+
uint64_t acc = 0x9E3779B97F4A7C15ULL;
49+
for (uint64_t i = 0; i < kIterations; i++) acc = xorshift(acc);
50+
slow_sink = acc;
51+
sem_signal();
52+
return nullptr;
53+
}
54+
55+
static void* fast_work(void*) {
56+
uint64_t acc = 0x9E3779B97F4A7C15ULL;
57+
for (uint64_t i = 0; i < kIterations / 2; i++) acc = xorshift(acc);
58+
fast_sink = acc;
59+
sem_signal();
60+
return nullptr;
61+
}
62+
63+
int main() {
64+
sem_setup();
65+
printf("Starting.\n");
66+
for (int round = 0; round < 100; round++) {
67+
pthread_t a, b;
68+
pthread_create(&a, nullptr, slow_work, nullptr);
69+
pthread_create(&b, nullptr, fast_work, nullptr);
70+
sem_wait_one(); // main blocks here -- invisible to coz without the wrappers
71+
sem_wait_one();
72+
pthread_detach(a);
73+
pthread_detach(b);
74+
COZ_PROGRESS;
75+
printf("."); fflush(stdout);
76+
}
77+
printf("\nDone. %llu %llu\n", (unsigned long long)slow_sink, (unsigned long long)fast_sink);
78+
}

libcoz/libcoz.cpp

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,46 @@ extern "C" {
452452
return real::pthread_mutex_unlock(mutex);
453453
}
454454

455+
/**
456+
* POSIX semaphores.
457+
*
458+
* A thread blocked on a semaphore is not running, so it must not be charged
459+
* for virtual delays inserted while it slept -- otherwise it pays them all at
460+
* once on wake-up, and if it is the thread that visits the progress point,
461+
* every line in the profile acquires a negative slope.
462+
*
463+
* glibc implements sem_wait on futex(2) with an inline syscall, so the futex
464+
* cannot be interposed. sem_wait can: it is an ordinary exported libc symbol.
465+
* That is the seam, and it covers everything layered on POSIX semaphores,
466+
* including swift-corelibs-libdispatch's DispatchSemaphore.
467+
*/
468+
int sem_wait(sem_t* sem) {
469+
if(initialized) profiler::get_instance().pre_block();
470+
int result = real::sem_wait(sem);
471+
// Woken by a sem_post from another thread, so skip the delays.
472+
if(initialized) profiler::get_instance().post_block(true);
473+
return result;
474+
}
475+
476+
int sem_timedwait(sem_t* sem, const struct timespec* abstime) {
477+
if(initialized) profiler::get_instance().pre_block();
478+
int result = real::sem_timedwait(sem, abstime);
479+
// On timeout nobody handed us the semaphore, so we own our delays.
480+
if(initialized) profiler::get_instance().post_block(result == 0);
481+
return result;
482+
}
483+
484+
/// Never blocks, so there is nothing to skip.
485+
int sem_trywait(sem_t* sem) {
486+
return real::sem_trywait(sem);
487+
}
488+
489+
/// May unblock another thread, so pay outstanding delays before it runs.
490+
int sem_post(sem_t* sem) {
491+
if(initialized) profiler::get_instance().catch_up();
492+
return real::sem_post(sem);
493+
}
494+
455495
/**
456496
* Enforce a floor on the alternate signal stack.
457497
*

0 commit comments

Comments
 (0)