Went from 4.139s to 2.413s. I fail to see how it is 70%. I think it is explained as 4.139/2.413 = 1.7 which of course doesn't make sense here.
Making a Go program faster with a one-character change
11–20 of 249 posts
Re: Making a Go program faster with a one-character change
#12That 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.
When it's a pointer to a copy, no such implicit dependencies occur.
Re: Making a Go program faster with a one-character change
#13That 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.
Re: Making a Go program faster with a one-character change
#14That 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 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
#151 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
Looks a bit like https://newcss.net/ or Water CSS
Re: Making a Go program faster with a one-character change
#16As 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
#17That 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.
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
#18That 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.…
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
#191 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
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.
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)
}