Commit 0d4b6ac741

Andrew Kelley <andrew@ziglang.org>
2021-01-23 07:36:30
add LTO support
The CLI gains -flto and -fno-lto options to override the default. However, the cool thing about this is that the defaults are great! In general when you use build-exe in release mode, Zig will enable LTO if it would work and it would help. zig cc supports detecting and honoring the -flto and -fno-lto flags as well. The linkWithLld functions are improved to all be the same with regards to copying the artifact instead of trying to pass single objects through LLD with -r. There is possibly a future improvement here as well; see the respective TODOs. stage1 is updated to support outputting LLVM bitcode instead of machine code when lto is enabled. This allows LLVM to optimize across the Zig and C/C++ code boundary. closes #2845
1 parent ab4f3ae
src/codegen/llvm/bindings.zig
@@ -243,7 +243,13 @@ pub const TargetMachine = opaque {
     extern fn LLVMDisposeTargetMachine(T: *const TargetMachine) void;
 
     pub const emitToFile = LLVMTargetMachineEmitToFile;
-    extern fn LLVMTargetMachineEmitToFile(*const TargetMachine, M: *const Module, Filename: [*:0]const u8, codegen: CodeGenFileType, ErrorMessage: *[*:0]const u8) LLVMBool;
+    extern fn LLVMTargetMachineEmitToFile(
+        *const TargetMachine,
+        M: *const Module,
+        Filename: [*:0]const u8,
+        codegen: CodeGenFileType,
+        ErrorMessage: *[*:0]const u8,
+    ) LLVMBool;
 };
 
 pub const CodeMode = extern enum {
src/link/Coff.zig
@@ -945,6 +945,13 @@ fn linkWithLLD(self: *Coff, comp: *Compilation) !void {
         if (!self.base.options.strip) {
             try argv.append("-DEBUG");
         }
+        if (self.base.options.lto) {
+            switch (self.base.options.optimize_mode) {
+                .Debug => {},
+                .ReleaseSmall => try argv.append("-OPT:lldlto=2"),
+                .ReleaseFast, .ReleaseSafe => try argv.append("-OPT:lldlto=3"),
+            }
+        }
         if (self.base.options.output_mode == .Exe) {
             const stack_size = self.base.options.stack_size_override orelse 16777216;
             try argv.append(try allocPrint(arena, "-STACK:{d}", .{stack_size}));
src/link/Elf.zig
@@ -1384,351 +1384,385 @@ fn linkWithLLD(self: *Elf, comp: *Compilation) !void {
         };
     }
 
-    // Create an LLD command line and invoke it.
-    var argv = std.ArrayList([]const u8).init(self.base.allocator);
-    defer argv.deinit();
-    // We will invoke ourselves as a child process to gain access to LLD.
-    // This is necessary because LLD does not behave properly as a library -
-    // it calls exit() and does not reset all global data between invocations.
-    try argv.appendSlice(&[_][]const u8{ comp.self_exe_path.?, "ld.lld" });
-    if (is_obj) {
-        try argv.append("-r");
-    }
+    const full_out_path = try directory.join(arena, &[_][]const u8{self.base.options.emit.?.sub_path});
+    if (self.base.options.output_mode == .Obj and self.base.options.lto) {
+        // In this case we must do a simple file copy
+        // here. TODO: think carefully about how we can avoid this redundant operation when doing
+        // build-obj. See also the corresponding TODO in linkAsArchive.
+        const the_object_path = blk: {
+            if (self.base.options.objects.len != 0)
+                break :blk self.base.options.objects[0];
+
+            if (comp.c_object_table.count() != 0)
+                break :blk comp.c_object_table.items()[0].key.status.success.object_path;
+
+            if (module_obj_path) |p|
+                break :blk p;
+
+            // TODO I think this is unreachable. Audit this situation when solving the above TODO
+            // regarding eliding redundant object -> object transformations.
+            return error.NoObjectsToLink;
+        };
+        // This can happen when using --enable-cache and using the stage1 backend. In this case
+        // we can skip the file copy.
+        if (!mem.eql(u8, the_object_path, full_out_path)) {
+            try fs.cwd().copyFile(the_object_path, fs.cwd(), full_out_path, .{});
+        }
+    } else {
 
-    try argv.append("-error-limit=0");
+        // Create an LLD command line and invoke it.
+        var argv = std.ArrayList([]const u8).init(self.base.allocator);
+        defer argv.deinit();
+        // We will invoke ourselves as a child process to gain access to LLD.
+        // This is necessary because LLD does not behave properly as a library -
+        // it calls exit() and does not reset all global data between invocations.
+        try argv.appendSlice(&[_][]const u8{ comp.self_exe_path.?, "ld.lld" });
+        if (is_obj) {
+            try argv.append("-r");
+        }
 
-    if (self.base.options.output_mode == .Exe) {
-        try argv.append("-z");
-        try argv.append(try std.fmt.allocPrint(arena, "stack-size={d}", .{stack_size}));
-    }
+        try argv.append("-error-limit=0");
 
-    if (self.base.options.image_base_override) |image_base| {
-        try argv.append(try std.fmt.allocPrint(arena, "--image-base={d}", .{image_base}));
-    }
+        if (self.base.options.lto) {
+            switch (self.base.options.optimize_mode) {
+                .Debug => {},
+                .ReleaseSmall => try argv.append("-O2"),
+                .ReleaseFast, .ReleaseSafe => try argv.append("-O3"),
+            }
+        }
 
-    if (self.base.options.linker_script) |linker_script| {
-        try argv.append("-T");
-        try argv.append(linker_script);
-    }
+        if (self.base.options.output_mode == .Exe) {
+            try argv.append("-z");
+            try argv.append(try std.fmt.allocPrint(arena, "stack-size={d}", .{stack_size}));
+        }
 
-    if (gc_sections) {
-        try argv.append("--gc-sections");
-    }
+        if (self.base.options.image_base_override) |image_base| {
+            try argv.append(try std.fmt.allocPrint(arena, "--image-base={d}", .{image_base}));
+        }
 
-    if (self.base.options.eh_frame_hdr) {
-        try argv.append("--eh-frame-hdr");
-    }
+        if (self.base.options.linker_script) |linker_script| {
+            try argv.append("-T");
+            try argv.append(linker_script);
+        }
 
-    if (self.base.options.emit_relocs) {
-        try argv.append("--emit-relocs");
-    }
+        if (gc_sections) {
+            try argv.append("--gc-sections");
+        }
 
-    if (self.base.options.rdynamic) {
-        try argv.append("--export-dynamic");
-    }
+        if (self.base.options.eh_frame_hdr) {
+            try argv.append("--eh-frame-hdr");
+        }
 
-    try argv.appendSlice(self.base.options.extra_lld_args);
+        if (self.base.options.emit_relocs) {
+            try argv.append("--emit-relocs");
+        }
 
-    if (self.base.options.z_nodelete) {
-        try argv.append("-z");
-        try argv.append("nodelete");
-    }
-    if (self.base.options.z_defs) {
-        try argv.append("-z");
-        try argv.append("defs");
-    }
+        if (self.base.options.rdynamic) {
+            try argv.append("--export-dynamic");
+        }
 
-    if (getLDMOption(target)) |ldm| {
-        // Any target ELF will use the freebsd osabi if suffixed with "_fbsd".
-        const arg = if (target.os.tag == .freebsd)
-            try std.fmt.allocPrint(arena, "{s}_fbsd", .{ldm})
-        else
-            ldm;
-        try argv.append("-m");
-        try argv.append(arg);
-    }
+        try argv.appendSlice(self.base.options.extra_lld_args);
 
-    if (self.base.options.link_mode == .Static) {
-        if (target.cpu.arch.isARM() or target.cpu.arch.isThumb()) {
-            try argv.append("-Bstatic");
-        } else {
-            try argv.append("-static");
+        if (self.base.options.z_nodelete) {
+            try argv.append("-z");
+            try argv.append("nodelete");
+        }
+        if (self.base.options.z_defs) {
+            try argv.append("-z");
+            try argv.append("defs");
         }
-    } else if (is_dyn_lib) {
-        try argv.append("-shared");
-    }
 
-    if (self.base.options.pie and self.base.options.output_mode == .Exe) {
-        try argv.append("-pie");
-    }
+        if (getLDMOption(target)) |ldm| {
+            // Any target ELF will use the freebsd osabi if suffixed with "_fbsd".
+            const arg = if (target.os.tag == .freebsd)
+                try std.fmt.allocPrint(arena, "{s}_fbsd", .{ldm})
+            else
+                ldm;
+            try argv.append("-m");
+            try argv.append(arg);
+        }
 
-    const full_out_path = try directory.join(arena, &[_][]const u8{self.base.options.emit.?.sub_path});
-    try argv.append("-o");
-    try argv.append(full_out_path);
+        if (self.base.options.link_mode == .Static) {
+            if (target.cpu.arch.isARM() or target.cpu.arch.isThumb()) {
+                try argv.append("-Bstatic");
+            } else {
+                try argv.append("-static");
+            }
+        } else if (is_dyn_lib) {
+            try argv.append("-shared");
+        }
 
-    if (link_in_crt) {
-        const crt1o: []const u8 = o: {
-            if (target.os.tag == .netbsd) {
-                break :o "crt0.o";
-            } else if (target.os.tag == .openbsd) {
-                if (self.base.options.link_mode == .Static) {
-                    break :o "rcrt0.o";
-                } else {
+        if (self.base.options.pie and self.base.options.output_mode == .Exe) {
+            try argv.append("-pie");
+        }
+
+        try argv.append("-o");
+        try argv.append(full_out_path);
+
+        if (link_in_crt) {
+            const crt1o: []const u8 = o: {
+                if (target.os.tag == .netbsd) {
                     break :o "crt0.o";
-                }
-            } else if (target.isAndroid()) {
-                if (self.base.options.link_mode == .Dynamic) {
-                    break :o "crtbegin_dynamic.o";
-                } else {
-                    break :o "crtbegin_static.o";
-                }
-            } else if (self.base.options.link_mode == .Static) {
-                if (self.base.options.pie) {
-                    break :o "rcrt1.o";
+                } else if (target.os.tag == .openbsd) {
+                    if (self.base.options.link_mode == .Static) {
+                        break :o "rcrt0.o";
+                    } else {
+                        break :o "crt0.o";
+                    }
+                } else if (target.isAndroid()) {
+                    if (self.base.options.link_mode == .Dynamic) {
+                        break :o "crtbegin_dynamic.o";
+                    } else {
+                        break :o "crtbegin_static.o";
+                    }
+                } else if (self.base.options.link_mode == .Static) {
+                    if (self.base.options.pie) {
+                        break :o "rcrt1.o";
+                    } else {
+                        break :o "crt1.o";
+                    }
                 } else {
-                    break :o "crt1.o";
+                    break :o "Scrt1.o";
                 }
-            } else {
-                break :o "Scrt1.o";
+            };
+            try argv.append(try comp.get_libc_crt_file(arena, crt1o));
+            if (target_util.libc_needs_crti_crtn(target)) {
+                try argv.append(try comp.get_libc_crt_file(arena, "crti.o"));
+            }
+            if (target.os.tag == .openbsd) {
+                try argv.append(try comp.get_libc_crt_file(arena, "crtbegin.o"));
             }
-        };
-        try argv.append(try comp.get_libc_crt_file(arena, crt1o));
-        if (target_util.libc_needs_crti_crtn(target)) {
-            try argv.append(try comp.get_libc_crt_file(arena, "crti.o"));
-        }
-        if (target.os.tag == .openbsd) {
-            try argv.append(try comp.get_libc_crt_file(arena, "crtbegin.o"));
         }
-    }
 
-    // rpaths
-    var rpath_table = std.StringHashMap(void).init(self.base.allocator);
-    defer rpath_table.deinit();
-    for (self.base.options.rpath_list) |rpath| {
-        if ((try rpath_table.fetchPut(rpath, {})) == null) {
-            try argv.append("-rpath");
-            try argv.append(rpath);
+        // rpaths
+        var rpath_table = std.StringHashMap(void).init(self.base.allocator);
+        defer rpath_table.deinit();
+        for (self.base.options.rpath_list) |rpath| {
+            if ((try rpath_table.fetchPut(rpath, {})) == null) {
+                try argv.append("-rpath");
+                try argv.append(rpath);
+            }
         }
-    }
-    if (self.base.options.each_lib_rpath) {
-        var test_path = std.ArrayList(u8).init(self.base.allocator);
-        defer test_path.deinit();
-        for (self.base.options.lib_dirs) |lib_dir_path| {
-            for (self.base.options.system_libs.items()) |entry| {
-                const link_lib = entry.key;
-                test_path.shrinkRetainingCapacity(0);
-                const sep = fs.path.sep_str;
-                try test_path.writer().print("{s}" ++ sep ++ "lib{s}.so", .{ lib_dir_path, link_lib });
-                fs.cwd().access(test_path.items, .{}) catch |err| switch (err) {
-                    error.FileNotFound => continue,
-                    else => |e| return e,
-                };
-                if ((try rpath_table.fetchPut(lib_dir_path, {})) == null) {
-                    try argv.append("-rpath");
-                    try argv.append(lib_dir_path);
+        if (self.base.options.each_lib_rpath) {
+            var test_path = std.ArrayList(u8).init(self.base.allocator);
+            defer test_path.deinit();
+            for (self.base.options.lib_dirs) |lib_dir_path| {
+                for (self.base.options.system_libs.items()) |entry| {
+                    const link_lib = entry.key;
+                    test_path.shrinkRetainingCapacity(0);
+                    const sep = fs.path.sep_str;
+                    try test_path.writer().print("{s}" ++ sep ++ "lib{s}.so", .{ lib_dir_path, link_lib });
+                    fs.cwd().access(test_path.items, .{}) catch |err| switch (err) {
+                        error.FileNotFound => continue,
+                        else => |e| return e,
+                    };
+                    if ((try rpath_table.fetchPut(lib_dir_path, {})) == null) {
+                        try argv.append("-rpath");
+                        try argv.append(lib_dir_path);
+                    }
                 }
             }
         }
-    }
 
-    for (self.base.options.lib_dirs) |lib_dir| {
-        try argv.append("-L");
-        try argv.append(lib_dir);
-    }
-
-    if (self.base.options.link_libc) {
-        if (self.base.options.libc_installation) |libc_installation| {
+        for (self.base.options.lib_dirs) |lib_dir| {
             try argv.append("-L");
-            try argv.append(libc_installation.crt_dir.?);
+            try argv.append(lib_dir);
         }
 
-        if (have_dynamic_linker) {
-            if (self.base.options.dynamic_linker) |dynamic_linker| {
-                try argv.append("-dynamic-linker");
-                try argv.append(dynamic_linker);
+        if (self.base.options.link_libc) {
+            if (self.base.options.libc_installation) |libc_installation| {
+                try argv.append("-L");
+                try argv.append(libc_installation.crt_dir.?);
             }
-        }
-    }
 
-    if (is_dyn_lib) {
-        if (self.base.options.soname) |soname| {
-            try argv.append("-soname");
-            try argv.append(soname);
-        }
-        if (self.base.options.version_script) |version_script| {
-            try argv.append("-version-script");
-            try argv.append(version_script);
+            if (have_dynamic_linker) {
+                if (self.base.options.dynamic_linker) |dynamic_linker| {
+                    try argv.append("-dynamic-linker");
+                    try argv.append(dynamic_linker);
+                }
+            }
         }
-    }
-
-    // Positional arguments to the linker such as object files.
-    try argv.appendSlice(self.base.options.objects);
 
-    for (comp.c_object_table.items()) |entry| {
-        try argv.append(entry.key.status.success.object_path);
-    }
+        if (is_dyn_lib) {
+            if (self.base.options.soname) |soname| {
+                try argv.append("-soname");
+                try argv.append(soname);
+            }
+            if (self.base.options.version_script) |version_script| {
+                try argv.append("-version-script");
+                try argv.append(version_script);
+            }
+        }
 
-    if (module_obj_path) |p| {
-        try argv.append(p);
-    }
+        // Positional arguments to the linker such as object files.
+        try argv.appendSlice(self.base.options.objects);
 
-    // TSAN
-    if (self.base.options.tsan) {
-        try argv.append(comp.tsan_static_lib.?.full_object_path);
-    }
+        for (comp.c_object_table.items()) |entry| {
+            try argv.append(entry.key.status.success.object_path);
+        }
 
-    // libc
-    // TODO: enable when stage2 can build c.zig
-    if (is_exe_or_dyn_lib and
-        !self.base.options.skip_linker_dependencies and
-        !self.base.options.link_libc and
-        build_options.is_stage1)
-    {
-        try argv.append(comp.libc_static_lib.?.full_object_path);
-    }
+        if (module_obj_path) |p| {
+            try argv.append(p);
+        }
 
-    // compiler-rt
-    if (compiler_rt_path) |p| {
-        try argv.append(p);
-    }
+        // TSAN
+        if (self.base.options.tsan) {
+            try argv.append(comp.tsan_static_lib.?.full_object_path);
+        }
 
-    // Shared libraries.
-    if (is_exe_or_dyn_lib) {
-        const system_libs = self.base.options.system_libs.items();
-        try argv.ensureCapacity(argv.items.len + system_libs.len);
-        for (system_libs) |entry| {
-            const link_lib = entry.key;
-            // By this time, we depend on these libs being dynamically linked libraries and not static libraries
-            // (the check for that needs to be earlier), but they could be full paths to .so files, in which
-            // case we want to avoid prepending "-l".
-            const ext = Compilation.classifyFileExt(link_lib);
-            const arg = if (ext == .shared_library) link_lib else try std.fmt.allocPrint(arena, "-l{s}", .{link_lib});
-            argv.appendAssumeCapacity(arg);
+        // libc
+        // TODO: enable when stage2 can build c.zig
+        if (is_exe_or_dyn_lib and
+            !self.base.options.skip_linker_dependencies and
+            !self.base.options.link_libc and
+            build_options.is_stage1)
+        {
+            try argv.append(comp.libc_static_lib.?.full_object_path);
         }
 
-        // libc++ dep
-        if (self.base.options.link_libcpp) {
-            try argv.append(comp.libcxxabi_static_lib.?.full_object_path);
-            try argv.append(comp.libcxx_static_lib.?.full_object_path);
+        // compiler-rt
+        if (compiler_rt_path) |p| {
+            try argv.append(p);
         }
 
-        // libc dep
-        if (self.base.options.link_libc) {
-            if (self.base.options.libc_installation != null) {
-                if (self.base.options.link_mode == .Static) {
-                    try argv.append("--start-group");
-                    try argv.append("-lc");
-                    try argv.append("-lm");
-                    try argv.append("--end-group");
-                } else {
-                    try argv.append("-lc");
-                    try argv.append("-lm");
-                }
+        // Shared libraries.
+        if (is_exe_or_dyn_lib) {
+            const system_libs = self.base.options.system_libs.items();
+            try argv.ensureCapacity(argv.items.len + system_libs.len);
+            for (system_libs) |entry| {
+                const link_lib = entry.key;
+                // By this time, we depend on these libs being dynamically linked libraries and not static libraries
+                // (the check for that needs to be earlier), but they could be full paths to .so files, in which
+                // case we want to avoid prepending "-l".
+                const ext = Compilation.classifyFileExt(link_lib);
+                const arg = if (ext == .shared_library) link_lib else try std.fmt.allocPrint(arena, "-l{s}", .{link_lib});
+                argv.appendAssumeCapacity(arg);
+            }
 
-                if (target.os.tag == .freebsd or target.os.tag == .netbsd or target.os.tag == .openbsd) {
-                    try argv.append("-lpthread");
-                }
-            } else if (target.isGnuLibC()) {
-                try argv.append(comp.libunwind_static_lib.?.full_object_path);
-                for (glibc.libs) |lib| {
-                    const lib_path = try std.fmt.allocPrint(arena, "{s}{c}lib{s}.so.{d}", .{
-                        comp.glibc_so_files.?.dir_path, fs.path.sep, lib.name, lib.sover,
-                    });
-                    try argv.append(lib_path);
+            // libc++ dep
+            if (self.base.options.link_libcpp) {
+                try argv.append(comp.libcxxabi_static_lib.?.full_object_path);
+                try argv.append(comp.libcxx_static_lib.?.full_object_path);
+            }
+
+            // libc dep
+            if (self.base.options.link_libc) {
+                if (self.base.options.libc_installation != null) {
+                    if (self.base.options.link_mode == .Static) {
+                        try argv.append("--start-group");
+                        try argv.append("-lc");
+                        try argv.append("-lm");
+                        try argv.append("--end-group");
+                    } else {
+                        try argv.append("-lc");
+                        try argv.append("-lm");
+                    }
+
+                    if (target.os.tag == .freebsd or target.os.tag == .netbsd or target.os.tag == .openbsd) {
+                        try argv.append("-lpthread");
+                    }
+                } else if (target.isGnuLibC()) {
+                    try argv.append(comp.libunwind_static_lib.?.full_object_path);
+                    for (glibc.libs) |lib| {
+                        const lib_path = try std.fmt.allocPrint(arena, "{s}{c}lib{s}.so.{d}", .{
+                            comp.glibc_so_files.?.dir_path, fs.path.sep, lib.name, lib.sover,
+                        });
+                        try argv.append(lib_path);
+                    }
+                    try argv.append(try comp.get_libc_crt_file(arena, "libc_nonshared.a"));
+                } else if (target.isMusl()) {
+                    try argv.append(comp.libunwind_static_lib.?.full_object_path);
+                    try argv.append(try comp.get_libc_crt_file(arena, switch (self.base.options.link_mode) {
+                        .Static => "libc.a",
+                        .Dynamic => "libc.so",
+                    }));
+                } else if (self.base.options.link_libcpp) {
+                    try argv.append(comp.libunwind_static_lib.?.full_object_path);
+                } else {
+                    unreachable; // Compiler was supposed to emit an error for not being able to provide libc.
                 }
-                try argv.append(try comp.get_libc_crt_file(arena, "libc_nonshared.a"));
-            } else if (target.isMusl()) {
-                try argv.append(comp.libunwind_static_lib.?.full_object_path);
-                try argv.append(try comp.get_libc_crt_file(arena, switch (self.base.options.link_mode) {
-                    .Static => "libc.a",
-                    .Dynamic => "libc.so",
-                }));
-            } else if (self.base.options.link_libcpp) {
-                try argv.append(comp.libunwind_static_lib.?.full_object_path);
-            } else {
-                unreachable; // Compiler was supposed to emit an error for not being able to provide libc.
             }
         }
-    }
 
-    // crt end
-    if (link_in_crt) {
-        if (target.isAndroid()) {
-            try argv.append(try comp.get_libc_crt_file(arena, "crtend_android.o"));
-        } else if (target.os.tag == .openbsd) {
-            try argv.append(try comp.get_libc_crt_file(arena, "crtend.o"));
-        } else if (target_util.libc_needs_crti_crtn(target)) {
-            try argv.append(try comp.get_libc_crt_file(arena, "crtn.o"));
+        // crt end
+        if (link_in_crt) {
+            if (target.isAndroid()) {
+                try argv.append(try comp.get_libc_crt_file(arena, "crtend_android.o"));
+            } else if (target.os.tag == .openbsd) {
+                try argv.append(try comp.get_libc_crt_file(arena, "crtend.o"));
+            } else if (target_util.libc_needs_crti_crtn(target)) {
+                try argv.append(try comp.get_libc_crt_file(arena, "crtn.o"));
+            }
         }
-    }
 
-    if (allow_shlib_undefined) {
-        try argv.append("--allow-shlib-undefined");
-    }
+        if (allow_shlib_undefined) {
+            try argv.append("--allow-shlib-undefined");
+        }
 
-    if (self.base.options.bind_global_refs_locally) {
-        try argv.append("-Bsymbolic");
-    }
+        if (self.base.options.bind_global_refs_locally) {
+            try argv.append("-Bsymbolic");
+        }
 
-    if (self.base.options.verbose_link) {
-        // Skip over our own name so that the LLD linker name is the first argv item.
-        Compilation.dump_argv(argv.items[1..]);
-    }
+        if (self.base.options.verbose_link) {
+            // Skip over our own name so that the LLD linker name is the first argv item.
+            Compilation.dump_argv(argv.items[1..]);
+        }
 
-    // Sadly, we must run LLD as a child process because it does not behave
-    // properly as a library.
-    const child = try std.ChildProcess.init(argv.items, arena);
-    defer child.deinit();
+        // Sadly, we must run LLD as a child process because it does not behave
+        // properly as a library.
+        const child = try std.ChildProcess.init(argv.items, arena);
+        defer child.deinit();
 
-    if (comp.clang_passthrough_mode) {
-        child.stdin_behavior = .Inherit;
-        child.stdout_behavior = .Inherit;
-        child.stderr_behavior = .Inherit;
+        if (comp.clang_passthrough_mode) {
+            child.stdin_behavior = .Inherit;
+            child.stdout_behavior = .Inherit;
+            child.stderr_behavior = .Inherit;
 
-        const term = child.spawnAndWait() catch |err| {
-            log.err("unable to spawn {s}: {s}", .{ argv.items[0], @errorName(err) });
-            return error.UnableToSpawnSelf;
-        };
-        switch (term) {
-            .Exited => |code| {
-                if (code != 0) {
-                    // TODO https://github.com/ziglang/zig/issues/6342
-                    std.process.exit(1);
-                }
-            },
-            else => std.process.abort(),
-        }
-    } else {
-        child.stdin_behavior = .Ignore;
-        child.stdout_behavior = .Ignore;
-        child.stderr_behavior = .Pipe;
+            const term = child.spawnAndWait() catch |err| {
+                log.err("unable to spawn {s}: {s}", .{ argv.items[0], @errorName(err) });
+                return error.UnableToSpawnSelf;
+            };
+            switch (term) {
+                .Exited => |code| {
+                    if (code != 0) {
+                        // TODO https://github.com/ziglang/zig/issues/6342
+                        std.process.exit(1);
+                    }
+                },
+                else => std.process.abort(),
+            }
+        } else {
+            child.stdin_behavior = .Ignore;
+            child.stdout_behavior = .Ignore;
+            child.stderr_behavior = .Pipe;
 
-        try child.spawn();
+            try child.spawn();
 
-        const stderr = try child.stderr.?.reader().readAllAlloc(arena, 10 * 1024 * 1024);
+            const stderr = try child.stderr.?.reader().readAllAlloc(arena, 10 * 1024 * 1024);
 
-        const term = child.wait() catch |err| {
-            log.err("unable to spawn {s}: {s}", .{ argv.items[0], @errorName(err) });
-            return error.UnableToSpawnSelf;
-        };
+            const term = child.wait() catch |err| {
+                log.err("unable to spawn {s}: {s}", .{ argv.items[0], @errorName(err) });
+                return error.UnableToSpawnSelf;
+            };
 
-        switch (term) {
-            .Exited => |code| {
-                if (code != 0) {
-                    // TODO parse this output and surface with the Compilation API rather than
-                    // directly outputting to stderr here.
-                    std.debug.print("{s}", .{stderr});
-                    return error.LLDReportedFailure;
-                }
-            },
-            else => {
-                log.err("{s} terminated with stderr:\n{s}", .{ argv.items[0], stderr });
-                return error.LLDCrashed;
-            },
-        }
+            switch (term) {
+                .Exited => |code| {
+                    if (code != 0) {
+                        // TODO parse this output and surface with the Compilation API rather than
+                        // directly outputting to stderr here.
+                        std.debug.print("{s}", .{stderr});
+                        return error.LLDReportedFailure;
+                    }
+                },
+                else => {
+                    log.err("{s} terminated with stderr:\n{s}", .{ argv.items[0], stderr });
+                    return error.LLDCrashed;
+                },
+            }
 
-        if (stderr.len != 0) {
-            log.warn("unexpected LLD stderr:\n{s}", .{stderr});
+            if (stderr.len != 0) {
+                log.warn("unexpected LLD stderr:\n{s}", .{stderr});
+            }
         }
     }
 
src/link/MachO.zig
@@ -620,6 +620,13 @@ fn linkWithLLD(self: *MachO, comp: *Compilation) !void {
             try argv.append("0");
         }
 
+        if (self.base.options.lto) {
+            switch (self.base.options.optimize_mode) {
+                .Debug => {},
+                .ReleaseSmall => try argv.append("-O2"),
+                .ReleaseFast, .ReleaseSafe => try argv.append("-O3"),
+            }
+        }
         try argv.append("-demangle");
 
         if (self.base.options.rdynamic and !self.base.options.system_linker_hack) {
src/link/Wasm.zig
@@ -362,122 +362,157 @@ fn linkWithLLD(self: *Wasm, comp: *Compilation) !void {
         };
     }
 
-    const is_obj = self.base.options.output_mode == .Obj;
-
-    // Create an LLD command line and invoke it.
-    var argv = std.ArrayList([]const u8).init(self.base.allocator);
-    defer argv.deinit();
-    // We will invoke ourselves as a child process to gain access to LLD.
-    // This is necessary because LLD does not behave properly as a library -
-    // it calls exit() and does not reset all global data between invocations.
-    try argv.appendSlice(&[_][]const u8{ comp.self_exe_path.?, "wasm-ld" });
-    if (is_obj) {
-        try argv.append("-r");
-    }
-
-    try argv.append("-error-limit=0");
+    const full_out_path = try directory.join(arena, &[_][]const u8{self.base.options.emit.?.sub_path});
 
-    if (self.base.options.output_mode == .Exe) {
-        // Increase the default stack size to a more reasonable value of 1MB instead of
-        // the default of 1 Wasm page being 64KB, unless overriden by the user.
-        try argv.append("-z");
-        const stack_size = self.base.options.stack_size_override orelse 1048576;
-        const arg = try std.fmt.allocPrint(arena, "stack-size={d}", .{stack_size});
-        try argv.append(arg);
-
-        // Put stack before globals so that stack overflow results in segfault immediately
-        // before corrupting globals. See https://github.com/ziglang/zig/issues/4496
-        try argv.append("--stack-first");
-    } else {
-        try argv.append("--no-entry"); // So lld doesn't look for _start.
-        try argv.append("--export-all");
-    }
-    try argv.appendSlice(&[_][]const u8{
-        "--allow-undefined",
-        "-o",
-        try directory.join(arena, &[_][]const u8{self.base.options.emit.?.sub_path}),
-    });
+    if (self.base.options.output_mode == .Obj) {
+        // LLD's WASM driver does not support the equvialent of `-r` so we do a simple file copy
+        // here. TODO: think carefully about how we can avoid this redundant operation when doing
+        // build-obj. See also the corresponding TODO in linkAsArchive.
+        const the_object_path = blk: {
+            if (self.base.options.objects.len != 0)
+                break :blk self.base.options.objects[0];
 
-    // Positional arguments to the linker such as object files.
-    try argv.appendSlice(self.base.options.objects);
+            if (comp.c_object_table.count() != 0)
+                break :blk comp.c_object_table.items()[0].key.status.success.object_path;
 
-    for (comp.c_object_table.items()) |entry| {
-        try argv.append(entry.key.status.success.object_path);
-    }
-    if (module_obj_path) |p| {
-        try argv.append(p);
-    }
+            if (module_obj_path) |p|
+                break :blk p;
 
-    if (self.base.options.output_mode != .Obj and
-        !self.base.options.skip_linker_dependencies and
-        !self.base.options.link_libc)
-    {
-        try argv.append(comp.libc_static_lib.?.full_object_path);
-    }
+            // TODO I think this is unreachable. Audit this situation when solving the above TODO
+            // regarding eliding redundant object -> object transformations.
+            return error.NoObjectsToLink;
+        };
+        // This can happen when using --enable-cache and using the stage1 backend. In this case
+        // we can skip the file copy.
+        if (!mem.eql(u8, the_object_path, full_out_path)) {
+            try fs.cwd().copyFile(the_object_path, fs.cwd(), full_out_path, .{});
+        }
+    } else {
+        const is_obj = self.base.options.output_mode == .Obj;
+
+        // Create an LLD command line and invoke it.
+        var argv = std.ArrayList([]const u8).init(self.base.allocator);
+        defer argv.deinit();
+        // We will invoke ourselves as a child process to gain access to LLD.
+        // This is necessary because LLD does not behave properly as a library -
+        // it calls exit() and does not reset all global data between invocations.
+        try argv.appendSlice(&[_][]const u8{ comp.self_exe_path.?, "wasm-ld" });
+        if (is_obj) {
+            try argv.append("-r");
+        }
 
-    if (compiler_rt_path) |p| {
-        try argv.append(p);
-    }
+        try argv.append("-error-limit=0");
 
-    if (self.base.options.verbose_link) {
-        // Skip over our own name so that the LLD linker name is the first argv item.
-        Compilation.dump_argv(argv.items[1..]);
-    }
+        if (self.base.options.lto) {
+            switch (self.base.options.optimize_mode) {
+                .Debug => {},
+                .ReleaseSmall => try argv.append("-O2"),
+                .ReleaseFast, .ReleaseSafe => try argv.append("-O3"),
+            }
+        }
 
-    // Sadly, we must run LLD as a child process because it does not behave
-    // properly as a library.
-    const child = try std.ChildProcess.init(argv.items, arena);
-    defer child.deinit();
+        if (self.base.options.output_mode == .Exe) {
+            // Increase the default stack size to a more reasonable value of 1MB instead of
+            // the default of 1 Wasm page being 64KB, unless overriden by the user.
+            try argv.append("-z");
+            const stack_size = self.base.options.stack_size_override orelse 1048576;
+            const arg = try std.fmt.allocPrint(arena, "stack-size={d}", .{stack_size});
+            try argv.append(arg);
+
+            // Put stack before globals so that stack overflow results in segfault immediately
+            // before corrupting globals. See https://github.com/ziglang/zig/issues/4496
+            try argv.append("--stack-first");
+        } else {
+            try argv.append("--no-entry"); // So lld doesn't look for _start.
+            try argv.append("--export-all");
+        }
+        try argv.appendSlice(&[_][]const u8{
+            "--allow-undefined",
+            "-o",
+            full_out_path,
+        });
 
-    if (comp.clang_passthrough_mode) {
-        child.stdin_behavior = .Inherit;
-        child.stdout_behavior = .Inherit;
-        child.stderr_behavior = .Inherit;
+        // Positional arguments to the linker such as object files.
+        try argv.appendSlice(self.base.options.objects);
 
-        const term = child.spawnAndWait() catch |err| {
-            log.err("unable to spawn {s}: {s}", .{ argv.items[0], @errorName(err) });
-            return error.UnableToSpawnSelf;
-        };
-        switch (term) {
-            .Exited => |code| {
-                if (code != 0) {
-                    // TODO https://github.com/ziglang/zig/issues/6342
-                    std.process.exit(1);
-                }
-            },
-            else => std.process.abort(),
+        for (comp.c_object_table.items()) |entry| {
+            try argv.append(entry.key.status.success.object_path);
+        }
+        if (module_obj_path) |p| {
+            try argv.append(p);
         }
-    } else {
-        child.stdin_behavior = .Ignore;
-        child.stdout_behavior = .Ignore;
-        child.stderr_behavior = .Pipe;
-
-        try child.spawn();
 
-        const stderr = try child.stderr.?.reader().readAllAlloc(arena, 10 * 1024 * 1024);
+        if (self.base.options.output_mode != .Obj and
+            !self.base.options.skip_linker_dependencies and
+            !self.base.options.link_libc)
+        {
+            try argv.append(comp.libc_static_lib.?.full_object_path);
+        }
 
-        const term = child.wait() catch |err| {
-            log.err("unable to spawn {s}: {s}", .{ argv.items[0], @errorName(err) });
-            return error.UnableToSpawnSelf;
-        };
+        if (compiler_rt_path) |p| {
+            try argv.append(p);
+        }
 
-        switch (term) {
-            .Exited => |code| {
-                if (code != 0) {
-                    // TODO parse this output and surface with the Compilation API rather than
-                    // directly outputting to stderr here.
-                    std.debug.print("{s}", .{stderr});
-                    return error.LLDReportedFailure;
-                }
-            },
-            else => {
-                log.err("{s} terminated with stderr:\n{s}", .{ argv.items[0], stderr });
-                return error.LLDCrashed;
-            },
+        if (self.base.options.verbose_link) {
+            // Skip over our own name so that the LLD linker name is the first argv item.
+            Compilation.dump_argv(argv.items[1..]);
         }
 
-        if (stderr.len != 0) {
-            log.warn("unexpected LLD stderr:\n{s}", .{stderr});
+        // Sadly, we must run LLD as a child process because it does not behave
+        // properly as a library.
+        const child = try std.ChildProcess.init(argv.items, arena);
+        defer child.deinit();
+
+        if (comp.clang_passthrough_mode) {
+            child.stdin_behavior = .Inherit;
+            child.stdout_behavior = .Inherit;
+            child.stderr_behavior = .Inherit;
+
+            const term = child.spawnAndWait() catch |err| {
+                log.err("unable to spawn {s}: {s}", .{ argv.items[0], @errorName(err) });
+                return error.UnableToSpawnSelf;
+            };
+            switch (term) {
+                .Exited => |code| {
+                    if (code != 0) {
+                        // TODO https://github.com/ziglang/zig/issues/6342
+                        std.process.exit(1);
+                    }
+                },
+                else => std.process.abort(),
+            }
+        } else {
+            child.stdin_behavior = .Ignore;
+            child.stdout_behavior = .Ignore;
+            child.stderr_behavior = .Pipe;
+
+            try child.spawn();
+
+            const stderr = try child.stderr.?.reader().readAllAlloc(arena, 10 * 1024 * 1024);
+
+            const term = child.wait() catch |err| {
+                log.err("unable to spawn {s}: {s}", .{ argv.items[0], @errorName(err) });
+                return error.UnableToSpawnSelf;
+            };
+
+            switch (term) {
+                .Exited => |code| {
+                    if (code != 0) {
+                        // TODO parse this output and surface with the Compilation API rather than
+                        // directly outputting to stderr here.
+                        std.debug.print("{s}", .{stderr});
+                        return error.LLDReportedFailure;
+                    }
+                },
+                else => {
+                    log.err("{s} terminated with stderr:\n{s}", .{ argv.items[0], stderr });
+                    return error.LLDCrashed;
+                },
+            }
+
+            if (stderr.len != 0) {
+                log.warn("unexpected LLD stderr:\n{s}", .{stderr});
+            }
         }
     }
 
src/stage1/all_types.hpp
@@ -2192,6 +2192,7 @@ struct CodeGen {
     bool is_single_threaded;
     bool have_pic;
     bool have_pie;
+    bool have_lto;
     bool link_mode_dynamic;
     bool dll_export_fns;
     bool have_stack_probing;
src/stage1/codegen.cpp
@@ -8449,8 +8449,9 @@ static void zig_llvm_emit_output(CodeGen *g) {
     // Unfortunately, LLVM shits the bed when we ask for both binary and assembly. So we call the entire
     // pipeline multiple times if this is requested.
     if (asm_filename != nullptr && bin_filename != nullptr) {
-        if (ZigLLVMTargetMachineEmitToFile(g->target_machine, g->module, &err_msg, g->build_mode == BuildModeDebug,
-            is_small, g->enable_time_report, g->tsan_enabled, nullptr, bin_filename, llvm_ir_filename))
+        if (ZigLLVMTargetMachineEmitToFile(g->target_machine, g->module, &err_msg,
+            g->build_mode == BuildModeDebug, is_small, g->enable_time_report, g->tsan_enabled,
+            g->have_lto, nullptr, bin_filename, llvm_ir_filename))
         {
             fprintf(stderr, "LLVM failed to emit file: %s\n", err_msg);
             exit(1);
@@ -8459,8 +8460,9 @@ static void zig_llvm_emit_output(CodeGen *g) {
         llvm_ir_filename = nullptr;
     }
 
-    if (ZigLLVMTargetMachineEmitToFile(g->target_machine, g->module, &err_msg, g->build_mode == BuildModeDebug,
-        is_small, g->enable_time_report, g->tsan_enabled, asm_filename, bin_filename, llvm_ir_filename))
+    if (ZigLLVMTargetMachineEmitToFile(g->target_machine, g->module, &err_msg,
+        g->build_mode == BuildModeDebug, is_small, g->enable_time_report, g->tsan_enabled,
+        g->have_lto, asm_filename, bin_filename, llvm_ir_filename))
     {
         fprintf(stderr, "LLVM failed to emit file: %s\n", err_msg);
         exit(1);
src/stage1/stage1.cpp
@@ -90,6 +90,7 @@ void zig_stage1_build_object(struct ZigStage1 *stage1) {
     g->dll_export_fns = stage1->dll_export_fns;
     g->have_pic = stage1->pic;
     g->have_pie = stage1->pie;
+    g->have_lto = stage1->lto;
     g->have_stack_probing = stage1->enable_stack_probing;
     g->red_zone = stage1->red_zone;
     g->is_single_threaded = stage1->is_single_threaded;
src/stage1/stage1.h
@@ -178,6 +178,7 @@ struct ZigStage1 {
 
     bool pic;
     bool pie;
+    bool lto;
     bool link_libc;
     bool link_libcpp;
     bool strip;
src/clang_options_data.zig
@@ -2732,7 +2732,14 @@ flagpd1("fkeep-static-consts"),
 flagpd1("flat_namespace"),
 flagpd1("flax-vector-conversions"),
 flagpd1("flimit-debug-info"),
-flagpd1("flto"),
+.{
+    .name = "flto",
+    .syntax = .flag,
+    .zig_equivalent = .lto,
+    .pd1 = true,
+    .pd2 = false,
+    .psl = false,
+},
 flagpd1("flto-unit"),
 flagpd1("flto-visibility-public-std"),
 sepd1("fmacro-backtrace-limit"),
@@ -2942,7 +2949,14 @@ flagpd1("fno-jump-tables"),
 flagpd1("fno-keep-static-consts"),
 flagpd1("fno-lax-vector-conversions"),
 flagpd1("fno-limit-debug-info"),
-flagpd1("fno-lto"),
+.{
+    .name = "fno-lto",
+    .syntax = .flag,
+    .zig_equivalent = .no_lto,
+    .pd1 = true,
+    .pd2 = false,
+    .psl = false,
+},
 flagpd1("fno-lto-unit"),
 flagpd1("fno-math-builtin"),
 flagpd1("fno-math-errno"),
@@ -5638,7 +5652,14 @@ jspd1("Ttext"),
     .pd2 = true,
     .psl = false,
 },
-joinpd1("flto="),
+.{
+    .name = "flto=",
+    .syntax = .joined,
+    .zig_equivalent = .lto,
+    .pd1 = true,
+    .pd2 = false,
+    .psl = false,
+},
 joinpd1("gcoff"),
 joinpd1("mabi="),
 joinpd1("mabs="),
src/Compilation.zig
@@ -444,6 +444,7 @@ pub const InitOptions = struct {
     want_valgrind: ?bool = null,
     want_tsan: ?bool = null,
     want_compiler_rt: ?bool = null,
+    want_lto: ?bool = null,
     use_llvm: ?bool = null,
     use_lld: ?bool = null,
     use_clang: ?bool = null,
@@ -602,6 +603,12 @@ pub fn create(gpa: *Allocator, options: InitOptions) !*Compilation {
             if (ofmt == .c)
                 break :blk false;
 
+            if (options.want_lto) |lto| {
+                if (lto) {
+                    break :blk true;
+                }
+            }
+
             // Our linker can't handle objects or most advanced options yet.
             if (options.link_objects.len != 0 or
                 options.c_source_files.len != 0 or
@@ -647,6 +654,26 @@ pub fn create(gpa: *Allocator, options: InitOptions) !*Compilation {
             break :outer opts;
         } else .{};
 
+        const lto = blk: {
+            if (options.want_lto) |explicit| {
+                if (!use_lld)
+                    return error.LtoUnavailableWithoutLld;
+                break :blk explicit;
+            } else if (!use_lld) {
+                break :blk false;
+            } else if (options.c_source_files.len == 0) {
+                break :blk false;
+            } else if (darwin_options.system_linker_hack) {
+                break :blk false;
+            } else switch (options.output_mode) {
+                .Lib, .Obj => break :blk false,
+                .Exe => switch (options.optimize_mode) {
+                    .Debug => break :blk false,
+                    .ReleaseSafe, .ReleaseFast, .ReleaseSmall => break :blk true,
+                },
+            }
+        };
+
         const tsan = options.want_tsan orelse false;
 
         const link_libc = options.link_libc or target_util.osRequiresLibC(options.target) or tsan;
@@ -821,6 +848,7 @@ pub fn create(gpa: *Allocator, options: InitOptions) !*Compilation {
         cache.hash.add(ofmt);
         cache.hash.add(pic);
         cache.hash.add(pie);
+        cache.hash.add(lto);
         cache.hash.add(tsan);
         cache.hash.add(stack_check);
         cache.hash.add(red_zone);
@@ -1022,6 +1050,7 @@ pub fn create(gpa: *Allocator, options: InitOptions) !*Compilation {
             .libc_installation = libc_dirs.libc_installation,
             .pic = pic,
             .pie = pie,
+            .lto = lto,
             .valgrind = valgrind,
             .tsan = tsan,
             .stack_check = stack_check,
@@ -2233,6 +2262,9 @@ pub fn addCCArgs(
                 "-nostdinc",
                 "-fno-spell-checking",
             });
+            if (comp.bin_file.options.lto) {
+                try argv.append("-flto");
+            }
 
             // According to Rich Felker libc headers are supposed to go before C language headers.
             // However as noted by @dimenus, appending libc headers before c_headers breaks intrinsics
@@ -3255,6 +3287,7 @@ fn updateStage1Module(comp: *Compilation, main_progress_node: *std.Progress.Node
         .err_color = @enumToInt(comp.color),
         .pic = comp.bin_file.options.pic,
         .pie = comp.bin_file.options.pie,
+        .lto = comp.bin_file.options.lto,
         .link_libc = comp.bin_file.options.link_libc,
         .link_libcpp = comp.bin_file.options.link_libcpp,
         .strip = comp.bin_file.options.strip,
@@ -3415,6 +3448,10 @@ pub fn build_crt_file(
         .want_tsan = false,
         .want_pic = comp.bin_file.options.pic,
         .want_pie = comp.bin_file.options.pie,
+        .want_lto = switch (output_mode) {
+            .Lib => comp.bin_file.options.lto,
+            .Obj, .Exe => false,
+        },
         .emit_h = null,
         .strip = comp.compilerRtStrip(),
         .is_native_os = comp.bin_file.options.is_native_os,
src/link.zig
@@ -74,6 +74,7 @@ pub const Options = struct {
     is_native_abi: bool,
     pic: bool,
     pie: bool,
+    lto: bool,
     valgrind: bool,
     tsan: bool,
     stack_check: bool,
src/main.zig
@@ -287,6 +287,8 @@ const usage_build_generic =
     \\  -fno-PIC                  Force-disable Position Independent Code
     \\  -fPIE                     Force-enable Position Independent Executable
     \\  -fno-PIE                  Force-disable Position Independent Executable
+    \\  -flto                     Force-enable Link Time Optimization (requires LLVM extensions)
+    \\  -fno-lto                  Force-disable Link Time Optimization
     \\  -fstack-check             Enable stack probing in unsafe builds
     \\  -fno-stack-check          Disable stack probing in safe builds
     \\  -fsanitize-c              Enable C undefined behavior detection in unsafe builds
@@ -511,6 +513,7 @@ fn buildOutputType(
     var enable_cache: ?bool = null;
     var want_pic: ?bool = null;
     var want_pie: ?bool = null;
+    var want_lto: ?bool = null;
     var want_sanitize_c: ?bool = null;
     var want_stack_check: ?bool = null;
     var want_red_zone: ?bool = null;
@@ -852,6 +855,10 @@ fn buildOutputType(
                         want_pie = true;
                     } else if (mem.eql(u8, arg, "-fno-PIE")) {
                         want_pie = false;
+                    } else if (mem.eql(u8, arg, "-flto")) {
+                        want_lto = true;
+                    } else if (mem.eql(u8, arg, "-fno-lto")) {
+                        want_lto = false;
                     } else if (mem.eql(u8, arg, "-fstack-check")) {
                         want_stack_check = true;
                     } else if (mem.eql(u8, arg, "-fno-stack-check")) {
@@ -1085,6 +1092,8 @@ fn buildOutputType(
                     .no_pic => want_pic = false,
                     .pie => want_pie = true,
                     .no_pie => want_pie = false,
+                    .lto => want_lto = true,
+                    .no_lto => want_lto = false,
                     .red_zone => want_red_zone = true,
                     .no_red_zone => want_red_zone = false,
                     .nostdlib => ensure_libc_on_non_freestanding = false,
@@ -1771,6 +1780,7 @@ fn buildOutputType(
         .link_libcpp = link_libcpp,
         .want_pic = want_pic,
         .want_pie = want_pie,
+        .want_lto = want_lto,
         .want_sanitize_c = want_sanitize_c,
         .want_stack_check = want_stack_check,
         .want_red_zone = want_red_zone,
@@ -2952,6 +2962,8 @@ pub const ClangArgIterator = struct {
         no_pic,
         pie,
         no_pie,
+        lto,
+        no_lto,
         nostdlib,
         nostdlib_cpp,
         shared,
src/stage1.zig
@@ -109,6 +109,7 @@ pub const Module = extern struct {
     err_color: ErrColor,
     pic: bool,
     pie: bool,
+    lto: bool,
     link_libc: bool,
     link_libcpp: bool,
     strip: bool,
src/zig_llvm.cpp
@@ -22,6 +22,7 @@
 
 #include <llvm/Analysis/TargetLibraryInfo.h>
 #include <llvm/Analysis/TargetTransformInfo.h>
+#include <llvm/Bitcode/BitcodeWriter.h>
 #include <llvm/IR/DIBuilder.h>
 #include <llvm/IR/DiagnosticInfo.h>
 #include <llvm/IR/IRBuilder.h>
@@ -184,7 +185,7 @@ unsigned ZigLLVMDataLayoutGetProgramAddressSpace(LLVMTargetDataRef TD) {
 
 bool ZigLLVMTargetMachineEmitToFile(LLVMTargetMachineRef targ_machine_ref, LLVMModuleRef module_ref,
         char **error_message, bool is_debug,
-        bool is_small, bool time_report, bool tsan,
+        bool is_small, bool time_report, bool tsan, bool lto,
         const char *asm_filename, const char *bin_filename, const char *llvm_ir_filename)
 {
     TimePassesIsEnabled = time_report;
@@ -234,7 +235,7 @@ bool ZigLLVMTargetMachineEmitToFile(LLVMTargetMachineRef targ_machine_ref, LLVMM
     PMBuilder->VerifyInput = assertions_on;
     PMBuilder->VerifyOutput = assertions_on;
     PMBuilder->MergeFunctions = !is_debug;
-    PMBuilder->PrepareForLTO = false;
+    PMBuilder->PrepareForLTO = lto;
     PMBuilder->PrepareForThinLTO = false;
     PMBuilder->PerformThinLTO = false;
 
@@ -272,7 +273,7 @@ bool ZigLLVMTargetMachineEmitToFile(LLVMTargetMachineRef targ_machine_ref, LLVMM
         PMBuilder->populateModulePassManager(MPM);
 
         // Set output passes.
-        if (dest_bin) {
+        if (dest_bin && !lto) {
             if (target_machine->addPassesToEmitFile(MPM, *dest_bin, nullptr, CGFT_ObjectFile)) {
                 *error_message = strdup("TargetMachine can't emit an object file");
                 return true;
@@ -299,6 +300,9 @@ bool ZigLLVMTargetMachineEmitToFile(LLVMTargetMachineRef targ_machine_ref, LLVMM
                 return true;
             }
         }
+        if (dest_bin && lto) {
+            WriteBitcodeToFile(*module, *dest_bin);
+        }
 
         if (time_report) {
             TimerGroup::printAll(errs());
src/zig_llvm.h
@@ -48,7 +48,7 @@ ZIG_EXTERN_C char *ZigLLVMGetNativeFeatures(void);
 
 ZIG_EXTERN_C bool ZigLLVMTargetMachineEmitToFile(LLVMTargetMachineRef targ_machine_ref, LLVMModuleRef module_ref,
         char **error_message, bool is_debug,
-        bool is_small, bool time_report, bool tsan,
+        bool is_small, bool time_report, bool tsan, bool lto,
         const char *asm_filename, const char *bin_filename, const char *llvm_ir_filename);
 
 
tools/update_clang_options.zig
@@ -62,6 +62,14 @@ const known_options = [_]KnownOpt{
         .name = "fno-PIE",
         .ident = "no_pie",
     },
+    .{
+        .name = "flto",
+        .ident = "lto",
+    },
+    .{
+        .name = "fno-lto",
+        .ident = "no_lto",
+    },
     .{
         .name = "nolibc",
         .ident = "nostdlib",