introduce --single-threaded build option

closes #1764

This adds another boolean to the test matrix; hopefully it does not
inflate the time too much.

std.event.Loop does not work with this option yet. See #1908
This commit is contained in:
Andrew Kelley 2019-02-01 17:49:29 -05:00
parent 8bedb10939
commit 9b8e23934b
No known key found for this signature in database
GPG Key ID: 7C5F548F728501A9
18 changed files with 244 additions and 81 deletions

View File

@ -6714,6 +6714,25 @@ pub fn build(b: *Builder) void {
{#header_close#}
{#see_also|Compile Variables|Zig Build System|Undefined Behavior#}
{#header_close#}
{#header_open|Single Threaded Builds#}
<p>Zig has a compile option <code>--single-threaded</code> which has the following effects:
<ul>
<li>{#link|@atomicLoad#} is emitted as a normal load.</li>
<li>{#link|@atomicRmw#} is emitted as a normal memory load, modify, store.</li>
<li>{#link|@fence#} becomes a no-op.</li>
<li>Variables which have Thread Local Storage instead become globals. TODO thread local variables
are not implemented yet.</li>
<li>The overhead of {#link|Coroutines#} becomes equivalent to function call overhead.
TODO: please note this will not be implemented until the upcoming Coroutine Rewrite</li>
<li>The {#syntax#}@import("builtin").single_threaded{#endsyntax#} becomes {#syntax#}true{#endsyntax#}
and therefore various userland APIs which read this variable become more efficient.
For example {#syntax#}std.Mutex{#endsyntax#} becomes
an empty data structure and all of its functions become no-ops.</li>
</ul>
</p>
{#header_close#}
{#header_open|Undefined Behavior#}
<p>
Zig has many instances of undefined behavior. If undefined behavior is

View File

@ -1808,6 +1808,7 @@ struct CodeGen {
bool is_static;
bool strip_debug_symbols;
bool is_test_build;
bool is_single_threaded;
bool is_native_target;
bool linker_rdynamic;
bool no_rosegment_workaround;

View File

@ -118,6 +118,7 @@ CodeGen *codegen_create(Buf *root_src_path, const ZigTarget *target, OutType out
g->string_literals_table.init(16);
g->type_info_cache.init(32);
g->is_test_build = false;
g->is_single_threaded = false;
buf_resize(&g->global_asm, 0);
for (size_t i = 0; i < array_length(symbols_that_llvm_depends_on); i += 1) {
@ -7377,6 +7378,7 @@ Buf *codegen_generate_builtin_source(CodeGen *g) {
buf_appendf(contents, "pub const endian = %s;\n", endian_str);
}
buf_appendf(contents, "pub const is_test = %s;\n", bool_to_str(g->is_test_build));
buf_appendf(contents, "pub const single_threaded = %s;\n", bool_to_str(g->is_single_threaded));
buf_appendf(contents, "pub const os = Os.%s;\n", cur_os);
buf_appendf(contents, "pub const arch = Arch.%s;\n", cur_arch);
buf_appendf(contents, "pub const environ = Environ.%s;\n", cur_environ);
@ -7411,6 +7413,7 @@ static Error define_builtin_compile_vars(CodeGen *g) {
cache_buf(&cache_hash, compiler_id);
cache_int(&cache_hash, g->build_mode);
cache_bool(&cache_hash, g->is_test_build);
cache_bool(&cache_hash, g->is_single_threaded);
cache_int(&cache_hash, g->zig_target.arch.arch);
cache_int(&cache_hash, g->zig_target.arch.sub_arch);
cache_int(&cache_hash, g->zig_target.vendor);
@ -8329,6 +8332,7 @@ static Error check_cache(CodeGen *g, Buf *manifest_dir, Buf *digest) {
cache_bool(ch, g->is_static);
cache_bool(ch, g->strip_debug_symbols);
cache_bool(ch, g->is_test_build);
cache_bool(ch, g->is_single_threaded);
cache_bool(ch, g->is_native_target);
cache_bool(ch, g->linker_rdynamic);
cache_bool(ch, g->no_rosegment_workaround);

View File

@ -59,6 +59,7 @@ static int print_full_usage(const char *arg0) {
" --release-fast build with optimizations on and safety off\n"
" --release-safe build with optimizations on and safety on\n"
" --release-small build with size optimizations on and safety off\n"
" --single-threaded source may assume it is only used single-threaded\n"
" --static output will be statically linked\n"
" --strip exclude debug symbols\n"
" --target-arch [name] specify target architecture\n"
@ -393,6 +394,7 @@ int main(int argc, char **argv) {
bool no_rosegment_workaround = false;
bool system_linker_hack = false;
TargetSubsystem subsystem = TargetSubsystemAuto;
bool is_single_threaded = false;
if (argc >= 2 && strcmp(argv[1], "build") == 0) {
Buf zig_exe_path_buf = BUF_INIT;
@ -550,6 +552,8 @@ int main(int argc, char **argv) {
disable_pic = true;
} else if (strcmp(arg, "--system-linker-hack") == 0) {
system_linker_hack = true;
} else if (strcmp(arg, "--single-threaded") == 0) {
is_single_threaded = true;
} else if (strcmp(arg, "--test-cmd-bin") == 0) {
test_exec_args.append(nullptr);
} else if (arg[1] == 'L' && arg[2] != 0) {
@ -816,6 +820,7 @@ int main(int argc, char **argv) {
switch (cmd) {
case CmdBuiltin: {
CodeGen *g = codegen_create(nullptr, target, out_type, build_mode, get_zig_lib_dir());
g->is_single_threaded = is_single_threaded;
Buf *builtin_source = codegen_generate_builtin_source(g);
if (fwrite(buf_ptr(builtin_source), 1, buf_len(builtin_source), stdout) != buf_len(builtin_source)) {
fprintf(stderr, "unable to write to stdout: %s\n", strerror(ferror(stdout)));
@ -889,6 +894,7 @@ int main(int argc, char **argv) {
codegen_set_out_name(g, buf_out_name);
codegen_set_lib_version(g, ver_major, ver_minor, ver_patch);
codegen_set_is_test(g, cmd == CmdTest);
g->is_single_threaded = is_single_threaded;
codegen_set_linker_script(g, linker_script);
if (each_lib_rpath)
codegen_set_each_lib_rpath(g, each_lib_rpath);

View File

@ -170,20 +170,36 @@ test "std.atomic.Queue" {
.get_count = 0,
};
var putters: [put_thread_count]*std.os.Thread = undefined;
for (putters) |*t| {
t.* = try std.os.spawnThread(&context, startPuts);
}
var getters: [put_thread_count]*std.os.Thread = undefined;
for (getters) |*t| {
t.* = try std.os.spawnThread(&context, startGets);
}
if (builtin.single_threaded) {
{
var i: usize = 0;
while (i < put_thread_count) : (i += 1) {
std.debug.assertOrPanic(startPuts(&context) == 0);
}
}
context.puts_done = 1;
{
var i: usize = 0;
while (i < put_thread_count) : (i += 1) {
std.debug.assertOrPanic(startGets(&context) == 0);
}
}
} else {
var putters: [put_thread_count]*std.os.Thread = undefined;
for (putters) |*t| {
t.* = try std.os.spawnThread(&context, startPuts);
}
var getters: [put_thread_count]*std.os.Thread = undefined;
for (getters) |*t| {
t.* = try std.os.spawnThread(&context, startGets);
}
for (putters) |t|
t.wait();
_ = @atomicRmw(u8, &context.puts_done, builtin.AtomicRmwOp.Xchg, 1, AtomicOrder.SeqCst);
for (getters) |t|
t.wait();
for (putters) |t|
t.wait();
_ = @atomicRmw(u8, &context.puts_done, builtin.AtomicRmwOp.Xchg, 1, AtomicOrder.SeqCst);
for (getters) |t|
t.wait();
}
if (context.put_sum != context.get_sum) {
std.debug.panic("failure\nput_sum:{} != get_sum:{}", context.put_sum, context.get_sum);

View File

@ -4,10 +4,13 @@ const AtomicOrder = builtin.AtomicOrder;
/// Many reader, many writer, non-allocating, thread-safe
/// Uses a spinlock to protect push() and pop()
/// When building in single threaded mode, this is a simple linked list.
pub fn Stack(comptime T: type) type {
return struct {
root: ?*Node,
lock: u8,
lock: @typeOf(lock_init),
const lock_init = if (builtin.single_threaded) {} else u8(0);
pub const Self = @This();
@ -19,7 +22,7 @@ pub fn Stack(comptime T: type) type {
pub fn init() Self {
return Self{
.root = null,
.lock = 0,
.lock = lock_init,
};
}
@ -31,20 +34,31 @@ pub fn Stack(comptime T: type) type {
}
pub fn push(self: *Self, node: *Node) void {
while (@atomicRmw(u8, &self.lock, builtin.AtomicRmwOp.Xchg, 1, AtomicOrder.SeqCst) != 0) {}
defer assert(@atomicRmw(u8, &self.lock, builtin.AtomicRmwOp.Xchg, 0, AtomicOrder.SeqCst) == 1);
if (builtin.single_threaded) {
node.next = self.root;
self.root = node;
} else {
while (@atomicRmw(u8, &self.lock, builtin.AtomicRmwOp.Xchg, 1, AtomicOrder.SeqCst) != 0) {}
defer assert(@atomicRmw(u8, &self.lock, builtin.AtomicRmwOp.Xchg, 0, AtomicOrder.SeqCst) == 1);
node.next = self.root;
self.root = node;
node.next = self.root;
self.root = node;
}
}
pub fn pop(self: *Self) ?*Node {
while (@atomicRmw(u8, &self.lock, builtin.AtomicRmwOp.Xchg, 1, AtomicOrder.SeqCst) != 0) {}
defer assert(@atomicRmw(u8, &self.lock, builtin.AtomicRmwOp.Xchg, 0, AtomicOrder.SeqCst) == 1);
if (builtin.single_threaded) {
const root = self.root orelse return null;
self.root = root.next;
return root;
} else {
while (@atomicRmw(u8, &self.lock, builtin.AtomicRmwOp.Xchg, 1, AtomicOrder.SeqCst) != 0) {}
defer assert(@atomicRmw(u8, &self.lock, builtin.AtomicRmwOp.Xchg, 0, AtomicOrder.SeqCst) == 1);
const root = self.root orelse return null;
self.root = root.next;
return root;
const root = self.root orelse return null;
self.root = root.next;
return root;
}
}
pub fn isEmpty(self: *Self) bool {
@ -90,20 +104,36 @@ test "std.atomic.stack" {
.get_count = 0,
};
var putters: [put_thread_count]*std.os.Thread = undefined;
for (putters) |*t| {
t.* = try std.os.spawnThread(&context, startPuts);
}
var getters: [put_thread_count]*std.os.Thread = undefined;
for (getters) |*t| {
t.* = try std.os.spawnThread(&context, startGets);
}
if (builtin.single_threaded) {
{
var i: usize = 0;
while (i < put_thread_count) : (i += 1) {
std.debug.assertOrPanic(startPuts(&context) == 0);
}
}
context.puts_done = 1;
{
var i: usize = 0;
while (i < put_thread_count) : (i += 1) {
std.debug.assertOrPanic(startGets(&context) == 0);
}
}
} else {
var putters: [put_thread_count]*std.os.Thread = undefined;
for (putters) |*t| {
t.* = try std.os.spawnThread(&context, startPuts);
}
var getters: [put_thread_count]*std.os.Thread = undefined;
for (getters) |*t| {
t.* = try std.os.spawnThread(&context, startGets);
}
for (putters) |t|
t.wait();
_ = @atomicRmw(u8, &context.puts_done, builtin.AtomicRmwOp.Xchg, 1, AtomicOrder.SeqCst);
for (getters) |t|
t.wait();
for (putters) |t|
t.wait();
_ = @atomicRmw(u8, &context.puts_done, builtin.AtomicRmwOp.Xchg, 1, AtomicOrder.SeqCst);
for (getters) |t|
t.wait();
}
if (context.put_sum != context.get_sum) {
std.debug.panic("failure\nput_sum:{} != get_sum:{}", context.put_sum, context.get_sum);

View File

@ -319,6 +319,9 @@ pub fn Channel(comptime T: type) type {
}
test "std.event.Channel" {
// https://github.com/ziglang/zig/issues/1908
if (builtin.single_threaded) return error.SkipZigTest;
var da = std.heap.DirectAllocator.init();
defer da.deinit();

View File

@ -84,6 +84,9 @@ pub fn Future(comptime T: type) type {
}
test "std.event.Future" {
// https://github.com/ziglang/zig/issues/1908
if (builtin.single_threaded) return error.SkipZigTest;
var da = std.heap.DirectAllocator.init();
defer da.deinit();

View File

@ -121,6 +121,9 @@ pub fn Group(comptime ReturnType: type) type {
}
test "std.event.Group" {
// https://github.com/ziglang/zig/issues/1908
if (builtin.single_threaded) return error.SkipZigTest;
var da = std.heap.DirectAllocator.init();
defer da.deinit();

View File

@ -122,6 +122,9 @@ pub const Lock = struct {
};
test "std.event.Lock" {
// https://github.com/ziglang/zig/issues/1908
if (builtin.single_threaded) return error.SkipZigTest;
var da = std.heap.DirectAllocator.init();
defer da.deinit();

View File

@ -97,6 +97,7 @@ pub const Loop = struct {
/// TODO copy elision / named return values so that the threads referencing *Loop
/// have the correct pointer value.
pub fn initMultiThreaded(self: *Loop, allocator: *mem.Allocator) !void {
if (builtin.single_threaded) @compileError("initMultiThreaded unavailable when building in single-threaded mode");
const core_count = try os.cpuCount(allocator);
return self.initInternal(allocator, core_count);
}
@ -201,6 +202,11 @@ pub const Loop = struct {
self.os_data.fs_thread.wait();
}
if (builtin.single_threaded) {
assert(extra_thread_count == 0);
return;
}
var extra_thread_index: usize = 0;
errdefer {
// writing 8 bytes to an eventfd cannot fail
@ -301,6 +307,11 @@ pub const Loop = struct {
self.os_data.fs_thread.wait();
}
if (builtin.single_threaded) {
assert(extra_thread_count == 0);
return;
}
var extra_thread_index: usize = 0;
errdefer {
_ = os.bsdKEvent(self.os_data.kqfd, final_kev_arr, empty_kevs, null) catch unreachable;
@ -338,6 +349,11 @@ pub const Loop = struct {
self.available_eventfd_resume_nodes.push(eventfd_node);
}
if (builtin.single_threaded) {
assert(extra_thread_count == 0);
return;
}
var extra_thread_index: usize = 0;
errdefer {
var i: usize = 0;
@ -845,6 +861,9 @@ pub const Loop = struct {
};
test "std.event.Loop - basic" {
// https://github.com/ziglang/zig/issues/1908
if (builtin.single_threaded) return error.SkipZigTest;
var da = std.heap.DirectAllocator.init();
defer da.deinit();
@ -858,6 +877,9 @@ test "std.event.Loop - basic" {
}
test "std.event.Loop - call" {
// https://github.com/ziglang/zig/issues/1908
if (builtin.single_threaded) return error.SkipZigTest;
var da = std.heap.DirectAllocator.init();
defer da.deinit();

View File

@ -269,6 +269,9 @@ pub async fn connect(loop: *Loop, _address: *const std.net.Address) !os.File {
}
test "listen on a port, send bytes, receive bytes" {
// https://github.com/ziglang/zig/issues/1908
if (builtin.single_threaded) return error.SkipZigTest;
if (builtin.os != builtin.Os.linux) {
// TODO build abstractions for other operating systems
return error.SkipZigTest;

View File

@ -211,6 +211,9 @@ pub const RwLock = struct {
};
test "std.event.RwLock" {
// https://github.com/ziglang/zig/issues/1908
if (builtin.single_threaded) return error.SkipZigTest;
var da = std.heap.DirectAllocator.init();
defer da.deinit();

View File

@ -14,7 +14,36 @@ const windows = std.os.windows;
/// If you need static initialization, use std.StaticallyInitializedMutex.
/// The Linux implementation is based on mutex3 from
/// https://www.akkadia.org/drepper/futex.pdf
pub const Mutex = switch(builtin.os) {
/// When an application is built in single threaded release mode, all the functions are
/// no-ops. In single threaded debug mode, there is deadlock detection.
pub const Mutex = if (builtin.single_threaded)
struct {
lock: @typeOf(lock_init),
const lock_init = if (std.debug.runtime_safety) false else {};
pub const Held = struct {
mutex: *Mutex,
pub fn release(self: Held) void {
if (std.debug.runtime_safety) {
self.mutex.lock = false;
}
}
};
pub fn init() Mutex {
return Mutex{ .lock = lock_init };
}
pub fn deinit(self: *Mutex) void {}
pub fn acquire(self: *Mutex) Held {
if (std.debug.runtime_safety and self.lock) {
@panic("deadlock detected");
}
return Held{ .mutex = self };
}
}
else switch (builtin.os) {
builtin.Os.linux => struct {
/// 0: unlocked
/// 1: locked, no waiters
@ -39,9 +68,7 @@ pub const Mutex = switch(builtin.os) {
};
pub fn init() Mutex {
return Mutex {
.lock = 0,
};
return Mutex{ .lock = 0 };
}
pub fn deinit(self: *Mutex) void {}
@ -60,7 +87,7 @@ pub const Mutex = switch(builtin.os) {
}
c = @atomicRmw(i32, &self.lock, AtomicRmwOp.Xchg, 2, AtomicOrder.Acquire);
}
return Held { .mutex = self };
return Held{ .mutex = self };
}
},
// TODO once https://github.com/ziglang/zig/issues/287 (copy elision) is solved, we can make a
@ -78,21 +105,19 @@ pub const Mutex = switch(builtin.os) {
mutex: *Mutex,
pub fn release(self: Held) void {
SpinLock.Held.release(SpinLock.Held { .spinlock = &self.mutex.lock });
SpinLock.Held.release(SpinLock.Held{ .spinlock = &self.mutex.lock });
}
};
pub fn init() Mutex {
return Mutex {
.lock = SpinLock.init(),
};
return Mutex{ .lock = SpinLock.init() };
}
pub fn deinit(self: *Mutex) void {}
pub fn acquire(self: *Mutex) Held {
_ = self.lock.acquire();
return Held { .mutex = self };
return Held{ .mutex = self };
}
},
};
@ -122,15 +147,20 @@ test "std.Mutex" {
.data = 0,
};
const thread_count = 10;
var threads: [thread_count]*std.os.Thread = undefined;
for (threads) |*t| {
t.* = try std.os.spawnThread(&context, worker);
}
for (threads) |t|
t.wait();
if (builtin.single_threaded) {
worker(&context);
std.debug.assertOrPanic(context.data == TestContext.incr_count);
} else {
const thread_count = 10;
var threads: [thread_count]*std.os.Thread = undefined;
for (threads) |*t| {
t.* = try std.os.spawnThread(&context, worker);
}
for (threads) |t|
t.wait();
std.debug.assertOrPanic(context.data == thread_count * TestContext.incr_count);
std.debug.assertOrPanic(context.data == thread_count * TestContext.incr_count);
}
}
fn worker(ctx: *TestContext) void {

View File

@ -3013,6 +3013,7 @@ pub const SpawnThreadError = error{
/// where T is u8, noreturn, void, or !void
/// caller must call wait on the returned thread
pub fn spawnThread(context: var, comptime startFn: var) SpawnThreadError!*Thread {
if (builtin.single_threaded) @compileError("cannot spawn thread when building in single-threaded mode");
// TODO compile-time call graph analysis to determine stack upper bound
// https://github.com/ziglang/zig/issues/157
const default_stack_size = 8 * 1024 * 1024;

View File

@ -40,6 +40,8 @@ fn testThreadIdFn(thread_id: *os.Thread.Id) void {
}
test "std.os.Thread.getCurrentId" {
if (builtin.single_threaded) return error.SkipZigTest;
var thread_current_id: os.Thread.Id = undefined;
const thread = try os.spawnThread(&thread_current_id, testThreadIdFn);
const thread_id = thread.handle();
@ -53,6 +55,8 @@ test "std.os.Thread.getCurrentId" {
}
test "spawn threads" {
if (builtin.single_threaded) return error.SkipZigTest;
var shared_ctx: i32 = 1;
const thread1 = try std.os.spawnThread({}, start1);

View File

@ -93,13 +93,18 @@ test "std.StaticallyInitializedMutex" {
.data = 0,
};
const thread_count = 10;
var threads: [thread_count]*std.os.Thread = undefined;
for (threads) |*t| {
t.* = try std.os.spawnThread(&context, TestContext.worker);
}
for (threads) |t|
t.wait();
if (builtin.single_threaded) {
TestContext.worker(&context);
std.debug.assertOrPanic(context.data == TestContext.incr_count);
} else {
const thread_count = 10;
var threads: [thread_count]*std.os.Thread = undefined;
for (threads) |*t| {
t.* = try std.os.spawnThread(&context, TestContext.worker);
}
for (threads) |t|
t.wait();
std.debug.assertOrPanic(context.data == thread_count * TestContext.incr_count);
std.debug.assertOrPanic(context.data == thread_count * TestContext.incr_count);
}
}

View File

@ -163,25 +163,32 @@ pub fn addPkgTests(b: *build.Builder, test_filter: ?[]const u8, root_src: []cons
for (test_targets) |test_target| {
const is_native = (test_target.os == builtin.os and test_target.arch == builtin.arch);
for (modes) |mode| {
for ([]bool{
false,
true,
}) |link_libc| {
if (link_libc and !is_native) {
// don't assume we have a cross-compiling libc set up
continue;
for ([]bool{ false, true }) |link_libc| {
for ([]bool{ false, true }) |single_threaded| {
if (link_libc and !is_native) {
// don't assume we have a cross-compiling libc set up
continue;
}
const these_tests = b.addTest(root_src);
these_tests.setNamePrefix(b.fmt(
"{}-{}-{}-{}-{}-{} ",
name,
@tagName(test_target.os),
@tagName(test_target.arch),
@tagName(mode),
if (link_libc) "c" else "bare",
if (single_threaded) "single" else "multi",
));
these_tests.setFilter(test_filter);
these_tests.setBuildMode(mode);
if (!is_native) {
these_tests.setTarget(test_target.arch, test_target.os, test_target.environ);
}
if (link_libc) {
these_tests.linkSystemLibrary("c");
}
step.dependOn(&these_tests.step);
}
const these_tests = b.addTest(root_src);
these_tests.setNamePrefix(b.fmt("{}-{}-{}-{}-{} ", name, @tagName(test_target.os), @tagName(test_target.arch), @tagName(mode), if (link_libc) "c" else "bare"));
these_tests.setFilter(test_filter);
these_tests.setBuildMode(mode);
if (!is_native) {
these_tests.setTarget(test_target.arch, test_target.os, test_target.environ);
}
if (link_libc) {
these_tests.linkSystemLibrary("c");
}
step.dependOn(&these_tests.step);
}
}
}