Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Don't assume a write if an operand is not in function parameters #18641

Merged
merged 1 commit into from
Jan 24, 2024

Conversation

favre49
Copy link
Contributor

@favre49 favre49 commented Jan 21, 2024

Liveness mistakenly assumes that if the operand is not in the parameters of a function call it is being written to, resulting in pointless memcpies.

New IR from the test case in #17580 - no memcpy:

; Function Attrs: nounwind uwtable
define internal fastcc i1 @bad.Foo.method(ptr nonnull readonly align 1 %0, i64 %1) unnamed_addr #0 {
  %3 = getelementptr inbounds %bad.Foo, ptr %0, i32 0, i32 0
  %4 = call fastcc i64 @bad.Foo.noop(i64 %1)
  %5 = getelementptr inbounds [100 x i1], ptr %3, i64 0, i64 %4
  %6 = load i1, ptr %5, align 1
  ret i1 %6
}

Difference in performance from the test case in #15685:

❯ zig run test.zig
error: Time: 49296208 Sum: 536854528

❯ zig-dev run test.zig
error: Time: 764281 Sum: 536854528

Fixes #17580, #15685

Liveness assumes that if the operand is not in the parameters of
a function call it is being written to, resulting in pointless memcpies.
@Jarred-Sumner
Copy link
Contributor

nice!

@andrewrk
Copy link
Member

Nice catch!

@andrewrk andrewrk merged commit 2ab7893 into ziglang:master Jan 24, 2024
10 checks passed
@mlugg
Copy link
Member

mlugg commented Jan 24, 2024

I'm a little confused about this change - the old logic seems correct, in that it absolutely is true that a function call can mutate arbitrary memory even if not passed as an argument (since a reference to that memory may be accessible through some other means, e.g. a global). Instead, I think the questionable thing is the usage of canElideLoad in the LLVM backend for by-ref types; this kind of acts as a half-baked solution to #5973, but not a good one. Looking more closely, it looks like the usages of canElideLoad and categorizeOperand are pretty incoherent in general, although perhaps I'm just misunderstanding how exactly they work.

Here is a code snippet whose behavior has (IMO incorrectly) changed after this merge:

const ByRef = struct {
    a: u32,
};

var global_ptr: *ByRef = undefined;

pub fn main() void {
    var x: ByRef = .{ .a = 1 };
    global_ptr = &x;
    const y = x; // we'd expect a guaranteed copy, so that this is guaranteed to be .{ .a = 1 }
    f(); // liveness thinks this call cannot mutate 'x' since it's not passed in, so can elide the load above
    @import("std").debug.print("{d}\n", .{y.a}); // so this really loads 'x', hence we read 42
}

fn f() void {
    global_ptr.* = .{ .a = 42 };
}

Before this PR, this program printed 1; now, it prints 42.

@andrewrk
Copy link
Member

I misread the original code nearby the patch, assuming the function was intended to only set .write if it matched something in the for loop. I see now something more subtle is happening. Apologies for the premature merge.

@andrewrk
Copy link
Member

andrewrk commented Jan 24, 2024

Reverted in 9d5a133.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

sometimes there is an unwanted memcpy when passing large structs by-value
4 participants