30 ms·
Making a Go program faster with a one-character change
- sendfoods 4y ago1 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
- gwd 4y ago> 1 character, in 2 places ;) Moving a single character from one place to another. :-) A good explanation of why "fire the developers with the lowest 50% of lines added" is an idiotic thing to do: this sort of deep analysis takes a lot of time and expertise, and frequently results in tiny changes.
- blowski 4y ago> is that theme custom or available somewhere Looks a bit like https://newcss.net/ https://newcss.net/ or Water CSS
- hcm 4y agoThanks! It's just a few dozen lines of CSS. The body font is Inter and the monospaced font is JetBrains Mono.
- lanstin 4y agoThat 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.
- deleted 4y ago[deleted]
- kgeist 4y agoI 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.
- derefr 4y agoYou'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.)
- oconnor663 4y agoI think the optimization is only valid if we know that nothing is ever going to use thr returned pointer to do mutation.
- ithkuil 4y agoThe 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.
- ithkuil 4y agoBTW; 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.
- ok123456 4y ago>In Rust you can write a function that returns the pointer of one element of a slice. Have fun fighting the borrow checker on that one.
- oconnor663 4y agoIt's not always so bad :) https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=097719766a5dc1ae2703813565793f5e https://play.rust-lang.org/?version=stable&mode=debug&editio...
- ok123456 4y agoNow try to actually do that in a larger program without static lifetimes...
- piperswe 4y agoIt shouldn't be too bad as long as you keep in mind that it is a reference to an item in that slice, so whatever that slice is pointing to needs to stick around as long as you're using elements from it. I don't often encounter borrow checker issues anymore, because once you program Rust long enough you know what things need to live for what lifetimes, and you architect your "larger programs" around that.
- runevault 4y agoNah an optimization is dangerous as others have said. A lint that detects oversized copies could be worthwhile though.
- infamousclyde 4y agoThank you for sharing. I'm curious if you would recommend any good resources for profiling with Go. I enjoyed your code snippets and methodology.
- hoosieree 4y ago> 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.
- masklinn 4y agoYou'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-remark-diagnostics 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 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 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 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<&Rule, Error> { 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<Box<Rule>, Error> { for rule in self.0.iter() { if rule.match_(path)? { return Ok(Box::new(rule.clone())); } } Err(Error) }
- _old_dude_ 4y agoIn Java -XX:+PrintCompilation -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining and -XX:+UnlockDiagnosticVMOptions -XX:+LogCompilation The generated logs can be seen with JITWatch [1]. [1] https://github.com/AdoptOpenJDK/jitwatch https://github.com/AdoptOpenJDK/jitwatch
- enedil 4y agoWent 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.
- morelisp 4y agoThis is an extremely common mistake in reporting performance numbers. That the old version is 70% slower does not make the new version 70% faster.
- wizofaus 4y ago70% slower is a bit ambiguous though - it could mean 70% extra runtime or it could mean 30% of the new speed. Whereas 70% faster would always suggest to me that it can do 70% more work in the same amount of time, i.e. a 1.7x increase in speed.
- enedil 4y agoI do not agree. When you benchmark you usually measure the differences between times needed to complete. This is because it is highly non-obvious that if you increase workload twice, the time increases also twice. Perhaps the algorithm is not linear. Perhaps if you have more data, you suddenly need to swap memory. Perhaps something (like disk access in parallel) means that actually it takes less than 2x time. This means that a concept of speed per unit of work is undefined. So the only reasonable interpretation of "70% faster" means "spends 30% time of original".
- yongjik 4y agoBut under your definition, if something becomes "three times as fast" (i.e., 200% faster), it will have to finish its task in negative time!
- wizofaus 4y agoI can categorically state I've never thought of or understand 70% faster as meaning that, and certainly not 100% faster as meaning "completes instantly". I see the OP has solved the problem by removing any references to how much faster from the article title! You're right about non-linear algorithms though. If an O(n^2) algorithm is 2x / 100% faster, it can't process 100% more items in the same time, but I'd understand it to mean taking half the time for the same n.
- coder543 4y agoThere 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/ 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.
- morelisp 4y agoPackages should be exposing an API with destination slices more often to begin with. The stdlib is pretty good about this (there's a few missing though 1.19 closed the most obvious absences), but most third-party code is awful. Or worse, it only takes strings.
- ploxiln 4y agoThat seems like overkill for this particular case, but it's a very interesting technique, thanks for the link!
- throwaway232iuu 4y agoWhy doesn't go use RVO like C++ and Rust? https://en.wikipedia.org/wiki/Copy_elision#Background https://en.wikipedia.org/wiki/Copy_elision#Background
- coder543 4y agoI don’t think we’re on the same page about what midstack inlining is being used for in my suggestion. This discussion is about eliminating a heap allocation, which as far as I understand, RVO never does. Please read the article I linked if you want to discuss this further. I don’t want to repeat the article pointlessly. I’m also fairly sure Go uses RVO here too, which cuts down on the number of times the object is copied around, but again, it’s irrelevant to the discussion of heap allocations. Copying the object isn’t the performance problem here, needlessly allocating a very short-lived object on the heap over and over is.
- cbsmith 4y agoAs an old C/C++ programmer, I'm always surprised by how often software developers are surprised by the performance costs of inopportune value semantics (C and C++ even more so, punishes you severely for using value semantics when you shouldn't). I increasingly see the wisdom of languages with implicit reference semantics. It's not that value semantics can't be better (they most assuredly can be), or that reference semantics don't cause their own complexity problems, but rather that so often we thoughtlessly imply/impose value semantics through interfaces in ways that negatively impact performance; getting interfaces wrong is a much tougher bell to unring. The vast majority of my mental energy when I define an interface in C++ is carefully thinking through a combination of ownership contracts and value vs. reference semantics that I can mostly ignore in languages with implicit reference semantics. While occasionally ignoring those contracts while developing in Java/Python/whatever comes back to bite me, the problem isn't nearly as common or problematic as when I unintentionally impose value semantics in a language that allows me to.
- throwaway894345 4y agoI also have a background in C/C++, etc and I've only ever found myself missing value semantics when I use languages with implicit reference semantics. I guess I always figured the solution was "value semantics with better education / tooling". Education: people should understand value semantics. Tooling: imagine an IDE that highlights allocation points automatically (or perhaps the problem is implicit allocations rather than value semantics?).
- TremendousJudge 4y ago>or perhaps the problem is implicit allocations rather than value semantics To me, this sounds like this is it. Explicit is better than implicit is a very useful truism
- cbsmith 4y agoThe counter argument to the "explicit is better than implicit" is that abstraction & encapsulation are such significant force multipliers. If done properly, implicit is good. It's just that in case of copying, doing it "properly" is well nigh impossible.
- assbuttbuttass 4y agoReturning a pointer to a local variable is convenient, but can be a source of hidden allocations. It's best to treat each struct as a "value" or "pointer" type, and use one or the other consistently for each type. This mostly avoids the need to use & in the first place
- asim 4y agoIf you want to have a solid understanding and need to do it in just a few hours here's a few things to review. - The Go programming language spec https://go.dev/ref/spec https://go.dev/ref/spec - Effective Go https://go.dev/doc/effective_go https://go.dev/doc/effective_go - Advanced Go concurrency patterns https://go.dev/talks/2013/advconc.slide#1 https://go.dev/talks/2013/advconc.slide#1 - Plus many more talks/slides https://go.dev/talks/ https://go.dev/talks/
- erdaniels 4y agoI created this video on concurrency (maybe advanced) patterns a while back that some may find helpful but it's pretty long https://www.youtube.com/watch?v=U3_2xiPxyA8 https://www.youtube.com/watch?v=U3_2xiPxyA8.
- lairv 4y agoThe "How to write Go code" article https://go.dev/doc/code https://go.dev/doc/code is also very useful to actually know how to structure a codebase
- amluto 4y agoSomewhat off topic, but I find a different part of this to be quite ugly: if match || err != nil { return rule, err } Translating this code to actual logic takes too much thought and is too fragile. Is that an error path or a success path? It’s both! The logic is “if we found a rule or if there was an error then return a tuple that hopefully indicates the outcome”. If any further code were to be added in this block, it would have to be validated for the success and the error case. But this only makes any sense at all if one is okay with reading Go result returns in their full generality. A canonical Go function returns either Success(value) or Error(err not nil, meaningless auxiliary value). And this code has “meaningless auxiliary value” != nil! In fact, it’s a pointer that likely escapes further into unrelated error handling code and thus complicates and kind of lifetime or escape analysis. I don’t use Go, but if I did, I think this part of the language would be my biggest peeve. Go has very little explicit error handling; fine, that’s a reasonable design decision. But Go’s error handing is incorrectly typed, and that is IMO not a reasonable design.
- ericbarrett 4y agoI write a lot of Go, and I agree that this is a big wart in its error handling that would be served by a proper Result type. Nevertheless, the convention is that if a function returns (value, err), and err != nil, the value is discarded (I think of it as "undefined"). So the code is conventional.
- amluto 4y agoIn C, “discarding” a pointer in a way that leaves the value visible is quite common. At least if one doesn’t accidentally use the pointer, it’s harmless. (In the way that all manner of unsafeness is harmless in C as long as no actual UB occurs, which is to say it’s not great.) But Go is a garbage collected language, and there is so such thing as “discarding” a pointer. Either it’s there or it isn’t, and this kind of leak has side effects. I find it baffling that the language designers and the community consider this acceptable. (One thing I really like about Rust is that you can’t play fast and loose with lifetimes like this. If you have function taking &'a Vec<T> and you return &'a T, you can’t arbitrarily “discard” and leak that reference up the call chain. You need to genuinely get rid of it by the time the vector is gone.)
- gp 4y agoI was trying to debug and improve the performance of some parallelized C++ code over the weekend for parsing CSV files. What would happen was parsing each file (~24k lines, 8 columns) would take 100ms with one execution context, but when split across many threads, the execution time of each thread would slow down proportionally and the throughput of the whole program would strictly decrease as thread count increased. I tried all of the obvious things, but the offender ended up being a call to allocate and fill a `struct tm` object from a string representation of a date. This doesn't have any obvious reasons (to me) that it would cause cache invalidation, etc, so I'm a little in the dark. Still, replacing this four line block improved single threaded performance by 5x, and fixed the threaded behavior, so on the whole it is now ~70x faster and parses about 400mb of csv per second.
- tonymet 4y agoOverall good review of profiling tactics . But there’s nothing egregious about Golang here . Pass by value vs reference is a common performance issue.
- masklinn 4y ago> But there’s nothing egregious about Golang here . Pass by value vs reference is a common performance issue. The trap here is that everything is passed by reference (pointer), but the intermediate local value is, well, a value (a copy). Rule is not a gigantic monster struct (it's 72 bytes), chances are returning it by value would not have been an issue. Anyway I would say there is an issue with Go here: it's way too easy to copy out of a slice.
- t3estabc 4y ago
- karmakaze 4y ago> I did consider two other approaches: Changing Ruleset from being []Rule to []*Rule, which would mean we no longer need to explicitly take a reference to the rule. Returning a Rule rather than a *Rule. This would still copy the Rule, but it should stay on the stack instead of moving to the heap. > However, both of these would have resulted in a breaking change as this method is part of the public API. The problem with heap allocated objects could be due to the incorrect public API. The change that improves performance also gives out pointers to the actual elements of Ruleset itself permitting the caller to change the contents of Ruleset which wasn't possible before the speed-up. Perhaps you're already aware since change to []*Rule was being considered.
- ok_dad 4y agoSometimes it doesn't matter if a public API is incorrect, because it's set in stone for whatever reason, and you just need to fix the problem internally.
- spockz 4y agoThis is why I like https://github.com/openrewrite https://github.com/openrewrite so much. One gets to tell users how to rewrite code automatically. It makes refactoring almost as easy as in a mono repo.
- bspammer 4y agoThis looks super interesting, but as far as I can tell they don’t go into how to inform downstream users to add your recipes to their maven/gradle configuration when you make a breaking change. In my head, the ideal flow would be an annotation on your library function/class which triggers an IntelliJ suggestion for downstream users affected by your breaking change to run the automated refactor for them. Kinda like a more helpful @Deprecated.
- spockz 4y agoIt would definitely be nice to have this generated from a deprecated like annotation! The maven plug-in seems to use the recipes directly as dependencies: https://github.com/openrewrite/rewrite-maven-plugin https://github.com/openrewrite/rewrite-maven-plugin In our internal situation the parent Pom could already control this plug-in definition including versions of the recipes. At the very least the recipes could follow the same version as the artefact. I thought I saw somewhere where the recipe was bundled with the artefact itself. That is very neat for simple usecase. However, it suffers from the same flaw as the native-image configuration for GraalVM in artifacts. Sometimes the configuration/recipe needs to change. Eg because of new insights or because it is incompatible with new versions of GraalVM/open rewrite. So I suppose having an extra version dimension Could solve this. Then one can always depend on the latest version of the recipe for that artifact.
- deleted 4y ago[deleted]
- chubot 4y agoFWIW, to prevent the bug where a = b is slow for big types, Google's C++ style guide used to mandate DISALLOW_COPY_AND_ASSIGN (which used to be DISALLOW_EVIL_CONSTRUCTORS I think) on all types (most types?) Looks like that's been gone for awhile in favor of C++ 11 stuff, which I don't really like: https://google.github.io/styleguide/cppguide.html#Copyable_Movable_Types https://google.github.io/styleguide/cppguide.html#Copyable_M... A lot of good software was written in that style, but it has grown bureaucratic over time, and as the C++ language evolved
- sakras 4y agoA while ago at my company we switched from GCC to Clang, and noticed a couple of massive regressions (on the order of 50%?) in performance having to do with floating point. After profiling for a bit, I discovered that suddenly a lot of time was spent in isinf on Clang and no time in GCC… Clang was emitting a function call where GCC wasn’t. I happened to randomly change isinf to std::isinf (it’s a random habit of mine to put std:: in front of these C functions). Suddenly the regression disappeared! I guess on Clang only std::isinf was a compiler intrinsic while GCC recognized both? Anyway, that’s my small-change optimization story.
- 10000truths 4y agoC defines isinf as a macro, whereas C++’s std::isinf is a function. Perhaps the discrepancy has to do with differences in how they’re evaluated?
- jeffrallen 4y ago> random habit of mine to put std:: in front of these C functions And did you learn your lesson about making random changes that "shouldn't matter" without proving they don't matter? :) I find that once I spend the time to make these changes correctly, they are not worth the time to make correctly.
- tuetuopay 4y agoAaaaaand that's why I love Rust's decision to make copies explicit with `.clone()`. Annoying as hell when you're not used to it but overall worth it.
- BlackFly 4y agoExcept a lot of structs also derive and prefer `Copy` and a lot of rust code also avoids heap allocation which requires `Clone`. The `Copy` trait can be used implicitly like in the example here. On the other hand, due to the lack of garbage collector, you wouldn't be able to return the reference to the copy which might lead you to find your accidental copy.
- tuetuopay 4y agoI have yet to come on structs that implement `Copy` while being expensive to actually copy. The largest I can think of is `Uuid` from the `uuid` crate, which is 128 bits in size. This is a single word copy for most machines since modern hardware has 128 bit support for case like this. Still, two 64-bit words to copy is definitely negligible: that's equivalent of copying two pointers. I agree with you for the garbage collector. By design, a GC allows you to willy-nilly copy without thinking about the consequences.
- kangalioo 4y agoThe Copy trait can only be used for bitwise copies. Expensive copying with heap allocations will never happen implicitly
- jimsmart 4y agoFrom the headline alone, I guessed this was to do with pointers/references to values vs values themselves. Yep, with values that take a lot of memory, it's faster to pass pointers/references around than it is to pass the values around, because it is less bytes to copy. Of course there is more to such a decision than just performance, because if the code makes changes to the value which are not meant to be persisted, then one wants to be working with a copy of the value, not a pointer to the value. So one should take care if simply switching some code from values to pointers-to-values. All of these things are things that coders with more experience of languages that use such semantics kinda know already, almost as second nature, since the first day they got caught out by them. But everyone is learning, to various degrees, and we all have to start somewhere (i.e. knowing little to nothing).
- lxe 4y agoThis is the kind of stuff that the compiler needs to really understand. If all this de-referencing and referencing magic is at the control of the user, it needs to have meaningful effect on what the code does. Otherwise we might as well just write C.
- silisili 4y agoThe compiler does understand it and did what was asked - it was just written rather poorly. There are valid use cases for wanting to take a copy, and then pass along a pointer of the copy. Perhaps to go through a series of modification methods that don't touch the original. I'd sure hate it if the compiler tried to outsmart me on that and changed the behavior away from what I'd written.
- AtNightWeCode 4y agoSo, this is very basic Go design and you could write something about how it works in C and Go and why a older lang like C don't have this prob but then at the end of the day the Go fanclub will down vote the hell out you no matter what.
- AtNightWeCode 4y agoGo compiler is garbage by the design. A 20 year old C compiler does not have this prob. This is also why Go have declined so much during the last couple of years. The benefits of Go have not increased and most of the quirks are still there. Like the error handling, the naive compiler and the syntax sugar that somewhat hides the diff between pointers and direct heap allocs. -1
- kosherhurricane 4y agoI work on a code base that is a mixture of Go and C. It's IO, CPU and Memory hungry, and it's distributed. C is fast because it's close to how CPU and memory actually work. Go gives you 95+% of that plus easy to learn, easy to use language. A new person could start contributing useful features and bug fixes immediately. A senior person could get C-level performance. More and more of our code is moved from C to Go, with very little performance penalty, but with a lot more safety and ease of use. Our customers benefit, and our company makes more money. In the end, that's what software is about.
- dist1ll 4y ago> C is fast because it's close to how CPU and memory actually work. Out-of-order execution, cache hierarchies, branch prediction, virtual memory, pipelining, vector instructions, ILP, NUMA are all pretty transparent to the C spec. Trying to accommodat hardware quirks with C feels like blackbox engineering. It's certainly better than with managed languages but still....
- kosherhurricane 4y ago
- is_taken 4y agoWould be interesting to see the performance difference if you undo that move-&-change and change the function signature from: func (r Ruleset) Match(path string) (*Rule, error) to: func (r *Ruleset) Match(path string) (*Rule, error)
- masklinn 4y agoLikely none: Ruleset is type Ruleset []Rule The original code creates a local copy of a rule and explicitly returns a pointer to that. Taking the ruleset by address wouldn't change that issue.
- Beltalowda 4y agoThe deeper lesson here is "don't use pointers unless you're sure you need them". I've seen quite a few people use pointers for no reason in particular, or there's simply the assumption it's faster (and have done this myself, too), but it puts a lot more pressure on the GC than simple local stack variables. Of course sometimes pointers are faster, or much more convenient. But as a rule of thumb: don't use pointers unless you've got a specific reason for them. This applies even more so if you're creating a lot of pointers (like in a loop, or a function that gets called very frequently).
- kccqzy 4y agoThat's not a deeper lesson. A deeper lesson is to understand where the pointer points to and then decide accordingly.
- Beltalowda 4y agoThat is pretty much what I said, except phrased different.
- throwaway894345 4y agoIt sounded to me like you were saying "avoid pointers by default" (bad advice IMO) rather than "if it matters, verify whether the pointer escapes" (good advice IMO).
- dist1ll 4y ago> but it puts a lot more pressure on the GC than simple local stack variables. Do you have evidence for this claim? AFAIK the Go compiler does escape analysis, and allocates pointers that don't escape on the stack.
- throwaway894345 4y agoThis is true, but it's hard to tell if a pointer escapes or not without actually profiling. That said, I don't think the answer is to avoid pointers, but rather to get comfortable with profiling the escape analyzer. By default, I just stick to the subset of Go which I know won't escape--functions can take pointer parameters, but I'm very careful about returning pointers to data that would otherwise be stack-allocated (even though it's not especially idiomatic, I'll often prefer mutating an `out T` parameter rather than returning a `T` because I know the former will not allocate).
- amtamt 4y agoIt falls in those 3% of code lines one should think of while not optimizing prematurely.
- ludiludi 4y ago> If you read the title and thought “well, you were probably just doing something silly beforehand”, you’re right! Don't feel too silly. Russ Cox, one of the technical leads on the Go language, made the same mistake in the regexp package of the standard library. https://go-review.googlesource.com/c/go/+/355789 https://go-review.googlesource.com/c/go/+/355789
- renewiltord 4y agoClear tutorial of how to go about identifying this. Good blog post. Since the problem was constrained and real, it helps someone know when to use these tools. Thank you for sharing.
- stephen123 4y agoGreat post. I always feel smart when I find these kind of optimisations. Then I wonder why the compiler isnt smarter, I dont have to be.
- erdaniels 4y agoIs there any nice tooling / static analysis for golang that instruments the builds the process to add all the gcflags with verbose output and give you hints as to what can be optimized?
- notpushkin 4y agoWell, technically it's either a 2-character or 0-character change! :-)
- cratermoon 4y agoThis has made me go back to look at all the Go I've written recently and look at the & uses.