Little things matter in Go

xuanbao · · 569 次点击    
这是一个分享于 的资源,其中的信息可能已经有所发展或是发生改变。
<p>I am a system administrator. These days, Go is the language I craft my larger, more complex tools with. I&#39;ve been using Go seriously for about a year-and-a-half now.</p> <p>Most of my previous programming experience, however, has been in languages like Perl and Python. When you work with those languages, prevailing wisdom is that it&#39;s almost never worth it to scrutinize the fine details of how data moves through programs. I don&#39;t mean that no attention is paid to data structure design or dataflow. I mean that casting and converting individual data from one type or format to another -- anytime, all the time, without much thought or concern -- is common.</p> <p>Part of this is because being loosely typed is a feature of those languages, so it&#39;s natural to treat data loosely, reshaping it as needed to make things easy on yourself. Another part of it is because in those languages, the runtime overhead provides a level of natural damping on theoretical performance gains from being careful to not, say, cast integers as a strings.</p> <p>In that milieu, optimization tends to involve things like using standard library code in place of hand-rolled code; finding wins from algorithm or code changes; moving from a pure-Perl/Python module to one with C bindings; or reworking clumsy structures into more easily manipulable ones, akin to refactoring a bad schema in an RDBMS.</p> <p>So I have been surprised to see the effects that being careful about accessing and structuring comparatively <em>tiny</em> pieces of data can have in Go.</p> <p>I&#39;ve been working on a networked application recently. Down at the very heart of it is a utility package I wrote which more-or-less does Unix shell style tokenizing on a slice of bytes. It gets called twice for every socket read; if it&#39;s slow then the application will definitely be slowed down. It&#39;s also one of the first &#34;serious&#34; pieces of Go code that I ever wrote, so it seemed like an excellent piece to go back to, and to try to find some cleanups or optimizations.</p> <p>The first thing I noticed when revisiting the code was that I seemed to have had an allergy to dealing with things as bytes when I initially wrote it. I was carelessly converting slices of bytes to strings, and then back, all over the place. I now know that&#39;s inefficient, so I did away with as many conversions as I could and rewrote the library internals to work entirely with byteslices. Strings are only produced by functions which return them.</p> <p>Then I did away with something which seemed pointlessly inclusive: to detect whitespace I was matching against the Unicode whitespace regexp class. I changed this to a simple equivalence check for the ASCII space or horizontal tab characters. This may need to be revisited one day, but for now the less flexible approach works fine.</p> <p>I wanted to see how much of a speedup (if any?) these changes would cause, so I had added some simple benchmarking routines to my test suite beforehand. I wasn&#39;t expecting more than a 20-30% increase, but what I saw was a 4-5X increase. That didn&#39;t seem possible, so I checked out the &#34;before&#34; version and reran the benchmarks several times. Then back to the new version and benchmarked again. The numbers stubbornly refused to change: the code was now around 400% faster than it had been originally, due to a tiny set of changes which I would have thought almost inconsequential.</p> <p>After that, I realized that I could do a more traditional optimization and rewrite a function to cut out unnecessary work in a common scenario. That didn&#39;t have anything to do with Go, but it got me another 1.8X speedup for that use case (on the benchmarked inputs; actual performance gain will scale with input size).</p> <p>Finally, I realized that this common case was very predictable, and instead of returning a slice, it could always return a three element array. Would this be worth it? Surely a variable <em>one to three</em> slice appends wouldn&#39;t be detectably slower than three array assignments. Wrong again: this change netted another 10% speedup, to 2X faster than the original implementation.</p> <p>The library is now 4 to 5 times faster all around, and 8 to 10 times faster in my most common case. All due to paying close attention to how I accessed and moved around very small pieces of data.</p> <p>My takeaway is that once you have working and correct code, it&#39;s worth reviewing to see if you&#39;ve been sloppy with data handling -- especially if you have a background in languages where this sort of scrutiny might be considered frivolous. In Go, it could make an unexpected difference.</p> <hr/>**评论:**<br/><br/>__crackers__: <pre><p>I&#39;m a Python coder trying to get into Go, and this is something I&#39;ve definitely noticed in the community.</p> <p>Optimisation is a much bigger topic amongst Gophers, which from my perspective, is amusing.</p> <p>The first program I ported to Go didn&#39;t seem to run significantly faster than the original Python, at a bit over 1 second. Then I realised I was using <code>go run script.go</code>, so the wallclock time included compilation, and the penny dropped when I also noticed the units in <code>log.Printf(&#34;done in %v&#34;, duration)</code> weren&#39;t seconds…</p> <p>The Go version was orders of magnitude faster. And these people want <em>more</em>???</p></pre>jmoiron: <pre><p>I think this might be largely historical. I can relate my own experience and that of many of my peers and attempt to generalise it, though I&#39;m not sure it&#39;s appropriate.</p> <p>3 years ago iron.io wrote one of the <a href="https://www.iron.io/how-we-went-from-30-servers-to-2-go/">earliest Go adoption posts</a> about replacing a ruby backend with Go and seeing 20-30x throughput improvements. About a year before that, <a href="http://commandcenter.blogspot.com/2012/06/less-is-exponentially-more.html">r wrote</a> (in what is <em>still</em> the best piece of writing about the Go philosophy and of Go adoption) that he was surprised to see dynamic language users adopting it.</p> <p>I think what we had was a time period where:</p> <ul> <li>Lots of backend programmers were around who had &#34;come of age&#34; with ruby and Python via rails and django (released 2004/2005)</li> <li>Lots of startups being built around &#34;big data&#34;, data science, and internet services that would require higher throughput</li> <li>Go was first to &#34;market&#34; with a compiled language that had a modern toolchain, painless build system, and no weird baggage</li> </ul> <p>All of these are kind of important. Companies that were already dealing with bigger data on the internet had already largely adopted compiled languages, even if they&#39;d started with dynamic ones: scala replacing ruby at twitter ca. 2008/9ish, java &amp; c++ with python at google ages ago, c++ at facebook via php compilation insanity.</p> <p>In this environment, if Go was to gain any traction, it&#39;d be first as a language to displace some of the brittle and slow infrastructure glue that had been getting written in Python and ruby. It proved to be really good at this, so people wrote about it.</p></pre>__crackers__: <pre><p>I think a lot of the Pythonistas getting into Go were prompted to look around by the extremely slow-motion train wreck that is Python 3.</p> <p>We were told we&#39;d have to rewrite all our programs for Py3 with very little to gain from it. In certain regards, Py3 doesn&#39;t so much solve Py2&#39;s issues as replace them with different, harder to solve problems.</p> <p>Py2 is in bugfix mode and Py3 is broken, so we went looking for an alternative, and Go is a great fit. It&#39;s philosophically attractive to the kind of person who likes Python and is vastly superior for many tasks.</p></pre>jmoiron: <pre><p>100% agree. When the announcement was made that 2.7 EOL was extended I tweeted:</p> <blockquote> <p>Last 5 years have been spent making the jump from 2.x to 3.x less difficult, not more compelling.</p> </blockquote> <p>There&#39;s certainly an element of that from a Python perspective. I don&#39;t have enough history/experience with Ruby to know if the 1.9 -&gt; 2.x transition was similar, from what I can tell it&#39;s not quite so fractured.</p> <p>I also think that the Python authors underestimate how restrictive the GIL is. I spent years believing that &#34;getting rid of the GIL would hurt single-CPU performance&#34;, and then here comes a language with better performance <em>and no</em> GIL hamstring in an 0.x version, and it worked just fine. Obviously it does not have the same semantics as Python and it&#39;s a bit apples/oranges, but it was <em>certainly</em> worth looking into.</p></pre>treeder123: <pre><p>The Ruby transition from 1-&gt;2 was barely noticeable. </p></pre>__crackers__: <pre><blockquote> <p>There&#39;s certainly an element of that from a Python perspective. I don&#39;t have enough history/experience with Ruby to know if the 1.9 -&gt; 2.x transition was similar, from what I can tell it&#39;s not quite so fractured.</p> </blockquote> <p>Not even close to Py2 vs 3. I&#39;m no Ruby guru, but my understanding is that while much new greatness was added to v2, pre-2 code runs just the same as ever because Ruby 2 adds new features rather than altering existing ones.</p> <blockquote> <p>I also think that the Python authors underestimate how restrictive the GIL is. I spent years believing that &#34;getting rid of the GIL would hurt single-CPU performance&#34;, and then here comes a language with better performance and no GIL hamstring in an 0.x version, and it worked just fine. Obviously it does not have the same semantics as Python and it&#39;s a bit apples/oranges, but it was certainly worth looking into.</p> </blockquote> <p>The thing I find most ironic/depressing is that Python had Go&#39;s concurrency model a decade ago with Stackless Python. Similarly, Twisted has been around for ~15 years, but still <code>asyncio</code> has somehow managed to steal its thunder, despite being over a decade late to the party and also a toy in comparison to Twisted.</p> <p>If Py3 had been stackless Python plus the new string model, instead of vanilla Py2 plus the new string model, I&#39;m 99% certain that Py3 would have been a roaring success, and Python developers wouldn&#39;t be coveting Go&#39;s channels etc.</p> <p>Sadly, it clings to the old execution model while breaking all manner of other shit that worked just fine in Py2.</p></pre>jasonwatkinspdx: <pre><blockquote> <p>Ruby to know if the 1.9 -&gt; 2.x transition was similar</p> </blockquote> <p>It was mostly painless to update apps. But even though in the end it was painless, it took <em>eons</em> for ruby to get a half decent JIT, despite there being a funded and officially blessed small team working on it since the pre rails days. I think a lot of us lost faith in ruby-dev&#39;s ability to keep pace with change over those years. They are very nice and very dedicated folks, but they&#39;re a bit isolated by language and culture from the huge rails community, let alone the larger FOSS world.</p></pre>kurin: <pre><p>I&#39;m working on a small library at work that has a function that will be invoked synchronously in every RPC call, which means all the goddamn time. This is the first time I&#39;ve ever been working on this scale. It&#39;s down to ~600ns per call.</p> <p>(The C++ version of the library, which we wrote first, clocked in at ~5us per call, which the RPC library team flat out told us was a no-go. That&#39;s down to &lt;200ns now.)</p></pre>__crackers__: <pre><p>Did you figure out why the C++ version was so much slower?</p> <p>I was being flippant wrt the speed of Go vs Python.</p> <p>It&#39;s really rather natural that optimisation is a bigger deal with Go than Python, simply because if execution speed is an absolute requirement, you wouldn&#39;t be using Python anyway.</p></pre>kurin: <pre><p>The C++ version was the initial implementation. After optimizations (data structures, etc) it became its lean self. The Go version was able to piggyback on the work done for the C++ version.</p></pre>princeandin: <pre><p><a href="http://img.pandawhale.com/post-40322-I-feel-the-need-for-speed-gif-JN84.gif" rel="nofollow"><strong>crackers</strong> we just want to Go FAST</a></p></pre>bestform: <pre><p>Very interesting. Thanks for the info and in depth analysis. This is yet another point, why it is so important to write good tests, write code, refactor code. Good thing you actually measured the performance instead of just observing that it is &#34;somewhat faster&#34; :)</p></pre>sboyette2: <pre><p>Thank you.</p></pre>b4ux1t3: <pre><p>Great write-up, but I do have one issue, which I&#39;ll put at the end and has nothing to do with the topic at hand.</p> <p>Go is the first language that I have used that made me feel comfortable working with raw data. And when I say raw, I mean literal bytes, as in your <del>application</del> library. Part of that comes from the fact that I found Go after I was already fairly competent at programming. I had a firm base of understanding under my feet before I even heard of Go, and as a result I was better able to grasp its concepts. But I don&#39;t think that&#39;s the only reason. (Also, all of this is coming from my personal experience. I have no references for any of this.)</p> <p>Go is just <em>friendly</em>. It&#39;s a tool that doesn&#39;t punish you for being wrong. If you screw up in C, for instance, you can severely damage your system (or, well, you could in the past, when I was starting to learn programming. I believe this has changed a lot, but I don&#39;t think that&#39;s a result of the language changing, just of operating systems being written better). I distinctly remember when I screwed up my first computer by writing what amounted to a virus completely by accident. (I wonder if I still have a copy of that program. I&#39;d be curious to see how well it works nowadays.)</p> <p>Again, that was a result of me being young and having no idea what I was doing. But I&#39;ve never had that problem with Go. I&#39;ve never gotten to the point where I write code that I don&#39;t understand. And part of that is just how well-thought out Go is. When I was trying to learn C, apart from the actual documentation by GNU, there were various and sundry resources out there that all said different things. There were a million and a half different voices out there. Go is different. </p> <p>It&#39;s all-in-one. It is its own compiler, its own documentation, and its own build framework. If you need information on how Go works, you go to one place: golang.org. And sure, there are plenty of great communities out there for all different languages, Go included. But many of those communities have, like I said before, a million and a half different resources that they all hold up as gospel. Not Go. If you ask a question about Go, people will point you to Go&#39;s docs directly.</p> <p>All of that leads to what I took away from your post: Working with raw data is so easy in Go because Go seems <em>designed</em> to work with raw data. From it&#39;s sane slice syntax to its brilliant</p> <p>That got a little ramble-y. Sorry about that. Normally when I sing Go&#39;s praises, I get yelled at for being a hipster.</p> <hr/> <p>Now, my one small issue with your post.</p> <blockquote> <p>Part of this is because being loosely typed is a feature of those languages</p> </blockquote> <p>Python is very specifically a strongly-typed language. It is also a dynamically-typed language. I believe Perl is both as well, but don&#39;t quote me on that.</p> <p>For instance, here&#39;s Javascript, a weakly(loosely)-typed language:</p> <h1>hello.js</h1> <pre><code>console.log(&#34;Hello&#34; + 1) </code></pre> <h1>output</h1> <pre><code>&#34;Hello1&#34; </code></pre> <p>And here is Python:</p> <h1>hello.py</h1> <pre><code>print(&#34;Hello&#34; + 1) </code></pre> <h1>output</h1> <pre><code>Traceback (most recent call last): File &#34;&lt;stdin&gt;&#34;, line 1, in &lt;module&gt; TypeError: cannot concatenate &#39;str&#39; and &#39;int&#39; objects </code></pre> <p>That said, you can do this, because it has dynamic typing:</p> <pre><code>i = 1 i = str(i) print(&#34;hello&#34; + i) </code></pre> <p>The performance hits you&#39;re seeing with Python have nothing (okay, not nothing, but very little) to do with the typing paradigm in Python. It used to be the case that the overhead of checking what each variable is at runtime cost a lot. However, as far as I know, the sheer power of our hardware coupled with some very clever tricks have offset that cost in all but a few situations.</p> <hr/> <p>Sorry, that&#39;s just one of those things that really irks me. As a programmer, talking about programming is a lot easier when everyone is using the same dictionary. Static/dynamic and strong/weak typing mean specific things.</p></pre>earthboundkid: <pre><p>Perl is a weakly typed language. It has a different operator for numerical addition and string concatenation, but ultimately strings and numbers are considered to be the same thing by the runtime and implicit numerical conversions happen all the time, which is the definition of weak typing.</p></pre>metamatic: <pre><p>With bloatware endemic, it&#39;s easy to forget that computers these days are <em>ridiculously</em> fast. For example, I needed to chop up a malformed XML file and send the chunks to an XML parser, so I wrote a file scanner which uses a buffered stream and a circular byte buffer to scan for arbitrary byte sequences, slice out the sections between them, and parse the results. When I ran it on my test files, it was basically instant.</p></pre>gerbs: <pre><p>Which is fine for an application that parses XML files, since it most likely won&#39;t run more than once a second.</p> <p>Where as an application that &#34;gets called twice for every socket read&#34; is going to have a bit more potential to bog down resources when running many concurrent operations.</p></pre>metamatic: <pre><blockquote> <p>Which is fine for an application that parses XML files, since it most likely won&#39;t run more than once a second.</p> </blockquote> <p>XMPP.</p> <p>Logging.</p></pre>fatAbb0t: <pre><p>Interesting stuff, thanks for sharing</p></pre>sboyette2: <pre><p>Thanks.</p></pre>ido50: <pre><p>Interesting post. I&#39;m a Perl developer now working with Go for a few months since starting a new job.</p> <p>Obviously we are in different worlds now, coming from a loosely typed language to a (use) strict one, but I&#39;m not sure the &#34;need for optimizations on smaller levels&#34; is that much different between Go and Perl or any language for that matter.</p> <p>As a Perl developer, I always revisit certain parts of code, trying to optimize them as much as possible, and over the years I&#39;ve optimized small and large parts of my own and colleague&#39;s code with significant gains.</p></pre>Partageons: <pre><p>I&#39;m probably going to get downvoted for this, but for your three-element array, would it be possible get the benefits of both by using</p> <pre><code>variable := make([]yourType, 0, 3) </code></pre> <p>?</p></pre>sboyette2: <pre><p>I considered that later on, and my intuition is to agree with you: the append() is the expensive part, not the use of a slice.</p></pre>cathalgarvey: <pre><p>Thank you! Curious: is the array/slce situation still noticeably different if you specify a cap of 3 when making the slice? i.e <code>fooslice := make([]foo, 0, 3)</code>?</p> <p>Or, for that matter, allocating a <em>length</em> of three and assigning like you are already with the array? I ask because using arrays directly is generally ugly when it interfaces other code, so keeping things slice-like is nice if it doesn&#39;t carry a performance hit..</p></pre>sboyette2: <pre><p>Agreed. See my response to <a href="/u/Partageons" rel="nofollow">u/Partageons</a> :)</p></pre>earthboundkid: <pre><p>I wrote a <a href="https://github.com/carlmjohnson/sudoku" rel="nofollow">Sudoku solver</a> in Go a couple of years ago, and I was tinkering with it again this week. It was based on a Python version written by someone else, so I figured I&#39;d go back and benchmark it for comparison. My Go version processes all the puzzles in the test suite in about 200ms. The Python version? At first, I assumed it was broken because it wasn&#39;t giving me an answer… Then I realized, no it works, it&#39;s just much, much slower. It took around 15 minutes to complete the test suite.</p> <p>I think the big difference is that the Go version is using bit flags for testing set membership and the Python version is using a <code>set</code> filled with strings for <code>1</code>, <code>2</code>, etc. Probably you could if you tried get the Python version to compete with the Go version if you used a <code>list</code> of <code>int</code> to do the calculations. But it&#39;s not natural to think about performance in the same ways in Python.</p></pre>schumacherfm: <pre><p>I&#39;m doing a test and benchmark driven development. Sure it takes more time but once you know how to optimize your code you instantly code the &#34;Go way&#34;. I&#39;m coming from PHP ;-)</p></pre>besna: <pre><p>Did you check that you are reusing already allocated memory as much as possible? I found out that this doesn&#39;t really improve the performance overall, but makes the deriviation between one benchmark run to another much smaller. You can see how many allocs you do with the go test benchmark runs if you enable the memory flags.</p></pre>sboyette2: <pre><p>I haven&#39;t looked at that sort of thing yet. It sounds interesting though. I should play with that soon.</p></pre>

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

569 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传