Live data from Hacker News

Making a Go program faster with a one-character change

hmarr.com

11–20 of 249 posts

Re: Making a Go program faster with a one-character change

#12
post #3

That seems like a potential for compiler optimization. It should already know that the rule value is only used one time, as the target of a & and this must be somewhat common in managing return values.

I don't think it can be optimized without altering semantics. If it's a pointer to a value in the slice, changing the slice's values (ruleset[i] = ...) will be reflected in all Rule values returned from the function, because they all point to the same memory. In the same way, changing the returned value's fields will change the data in the original slice. The author's code is prone to this behavior after the change.

When it's a pointer to a copy, no such implicit dependencies occur.

Re: Making a Go program faster with a one-character change

#13
post #3

That seems like a potential for compiler optimization. It should already know that the rule value is only used one time, as the target of a & and this must be somewhat common in managing return values.

I think the optimization is only valid if we know that nothing is ever going to use thr returned pointer to do mutation.

Re: Making a Go program faster with a one-character change

#14
post #3

That seems like a potential for compiler optimization. It should already know that the rule value is only used one time, as the target of a & and this must be somewhat common in managing return values.

The semantics change. You're now returning a pointer to the actual Rule in the Ruleset, while before you'd be returning a pointer to copy of the Rule.

The optimization would only work if you had a way to tell the compiler that some values are constant/immutable.

Re: Making a Go program faster with a one-character change

#15

1 character, in 2 places ;) I did not know profiling support for go was so seamless, thank you! May I ask, is that theme custom or available somewhere? I really enjoyed it

> is that theme custom or available somewhere

Looks a bit like https://newcss.net/ or Water CSS

Re: Making a Go program faster with a one-character change

#16
There is potentially another option: use the midstack inliner to move the allocation from the heap to the stack of the calling function: https://words.filippo.io/efficient-go-apis-with-the-inliner/

As long as the global slice is never mutated, the current approach is probably fine, but it is definitely a semantic change to the code.

Re: Making a Go program faster with a one-character change

#17
post #14
post #3

That seems like a potential for compiler optimization. It should already know that the rule value is only used one time, as the target of a & and this must be somewhat common in managing return values.

The semantics change. You're now returning a pointer to the actual Rule in the Ruleset, while before you'd be returning a pointer to copy of the Rule. The optimization would only work if you had a way to tell the compiler that some values are constant/immutable.

BTW; I'm using both Go and Rust lately.

In Rust you can write a function that returns the pointer of one element of a slice. You can also write a function that returns the pointer to a heap-allocated copy of an element of the slice. The two functions would have different signatures.

The compiler would also prevent mutation of the slice as long as there are any references to individual elements of the slice being passed around.

Re: Making a Go program faster with a one-character change

#18
post #12
post #3

That seems like a potential for compiler optimization. It should already know that the rule value is only used one time, as the target of a & and this must be somewhat common in managing return values.

I don't think it can be optimized without altering semantics. If it's a pointer to a value in the slice, changing the slice's values (ruleset[i] = ...) will be reflected in all Rule values returned from the function, because they all point to the same memory. In the same way, changing the returned value's fields will change the data in the original slice. The author's code is prone to this behavior after the change.…

You're not wrong in general, but one interesting thing about Go as an ecosystem (rather than as a language) is that golang programs are mostly statically compiled — all sources, one pass, one code unit, one object-code output — so they're (theoretically) very amenable to compile-time (rather than link-time) Whole-Program Optimization techniques.

In this specific case, that technique would be whole-program dataflow analysis. Given a Golang function that passes out references-to-copies-of owned data, you could actually determine for certain — at least in the default static-binary linkage mode — whether these two properties hold universally within the resulting binary:

1. whether no caller of the function will ever try to do anything that would cause data within their copy of the struct to be modified;

2. whether the owner of the data will never modify the data of the original struct in such a way that, if the copy were elided, the changes would be "seen" by any reads done in any of the callers. (The owner could still modify internal metadata within the struct for its own use, as long as such internal metadata is 1. in private fields, 2. where all callers live outside the package defining the struct, making those fields inaccessible; and 3. the fields are never accessed by any struct methods called by borrowers of the struct — keeping in mind that such methods can be defined outside the package by caller code.)

If you could prove both of these properties (using dataflow analysis), then you could safely elide the copy within the function, turning the return of a reference-to-a-copy-of-X into a return of a reference-to-X.

(And, in fact, if you can only prove the second property universally, and the first property in specific instances, then you can still elide the copy from the function itself; but you'd also generate a wrapper function that calls said function [receiving a reference-to-X], copies, and so returns a reference-to-a-copy-of-X; and then, for any call-site where the first property doesn't hold — i.e. callers whose transitive call-graph will ever modify the data — you'd replace the call to the original function with a call to the wrapper. So "safe" connected caller sub-graphs would receive references, while "unsafe" connected caller sub-graphs would receive copies.)

Re: Making a Go program faster with a one-character change

#19

1 character, in 2 places ;) I did not know profiling support for go was so seamless, thank you! May I ask, is that theme custom or available somewhere? I really enjoyed it

Thanks! It's just a few dozen lines of CSS. The body font is Inter and the monospaced font is JetBrains Mono.

Re: Making a Go program faster with a one-character change

#20

> You can see these decisions being made by passing -gcflags=-m to go build: That's a very nice feature! I wonder if compilers for other languages have something similar.

You'd have to see if all compilers support it, but LLVM has a "remarks" system, which should provide similar information (though likely a lot more of it) for optimization passes which are traced: https://llvm.org/docs/Remarks.html#introduction-to-the-llvm-...

The frontend may or may not have its own optimizations and logs tho e.g. rustc now has MIR optimizations (https://rustc-dev-guide.rust-lang.org/mir/optimizations.html) but while you can dump MIR data (https://rustc-dev-guide.rust-lang.org/mir/debugging.html) I don't remember seeing an optimisation log.

At the end of the day, I think it's more likely that you take a look at the assembly and infer problems from there if the profiler doesn't tell you straight. An other difference is the kind of decisions the compiler makes e.g. while a compiler can optimize away allocations in "manual allocation" languages (https://godbolt.org/z/5nEo7xjEr) the allocations are plainly visible, so if they're trivially avoidable... you'll just avoid them.

Using Rust as an example, you'd have something like this:

    pub fn match_(&self, path: &str) -> Result {
        for rule in self.0.iter() {
            if rule.match_(path)? {
                return Ok(rule);
            }
        }
        Err(Error)
    }
You couldn't miss an allocation, because the return type would have to change, and you'd need to perform the copy out:

    pub fn match_(&self, path: &str) -> Result, Error> {
        for rule in self.0.iter() {
            if rule.match_(path)? {
                return Ok(Box::new(rule.clone()));
            }
        }
        Err(Error)
    }
Post reply on HN