[HN Gopher] Taming Go's Memory Usage, or How We Avoided Rewritin...
       ___________________________________________________________________
        
       Taming Go's Memory Usage, or How We Avoided Rewriting Our Client in
       Rust
        
       Author : jeanyang
       Score  : 198 points
       Date   : 2021-09-21 18:25 UTC (4 hours ago)
        
 (HTM) web link (www.akitasoftware.com)
 (TXT) w3m dump (www.akitasoftware.com)
        
       | notamy wrote:
       | Bit confused by this part of the article:
       | 
       | > PRO-REWRITE: Rust has manual memory management, so we would
       | avoid the problem of having to wrestle with a garbage collector
       | because we would just deallocate unused memory ourselves, or more
       | carefully be able to engineer the response to increased load.
       | 
       | > ANTI-REWRITE: Rust has manual memory management, which means
       | that whenever we're writing code we'll have to take the time to
       | manage memory ourselves.
       | 
       | Isn't part of the point of Rust that you _don 't_ manage memory
       | yourself, and rather that the compiler is smart enough to manage
       | it for you?
        
         | saghm wrote:
         | There are already a lot of replies to this comment explaining
         | the ideas behind Rust memory management in different ways, but
         | I'll throw in my handwavy explanation as well:
         | 
         | In GC languages, memory management is generally runtime through
         | the interpreter/runtime. In C, memory management is generally
         | done at programming time by the (human) programmer. In Rust,
         | memory management is generally done at compile time by the
         | compiler. There are exceptions in all three cases, but the
         | "default" paradigm of a language informs a lot about how it's
         | designed and used.
        
         | NovemberWhiskey wrote:
         | Go = you do no explicit memory management and the GC/runtime
         | takes care of it for you
         | 
         | Rust = when writing your code, you explicitly describe the
         | ownership and lifetime of your objects and how your functions
         | are allowed to consume/copy etc. them and get safety as a
         | result
         | 
         | C = when writing your code, you explicitly allocate and free
         | your objects and you get no assistance from the language about
         | when it is safe to copy/dereference/free/etc. a
         | pointer/allocation
        
           | throwaway894345 wrote:
           | I prefer to think that in Go you don't do explicit memory
           | management _by default_ , while in Rust you do. Although you
           | _can_ laboriously opt out of explicit memory management
           | (e.g., by tagging everything Rc <> or Gc<> and all of the
           | ceremony that entails).
        
         | Spartan-S63 wrote:
         | I feel that this is one of those common misconceptions about
         | Rust. Rust's memory management is nothing like C or non-modern
         | C++'s with malloc/free or new/delete. Rust uses modern-C++'s
         | RAII model, typically, to allocate memory. The compiler is
         | smart enough to know when to call drop() (which is essentially
         | free/delete, but with the possibility of additional behavior).
         | You can also call drop() yourself.
         | 
         | What I think people _should_ focus on with Rust versus Go (et
         | al) is that Rust allows you to choose where you _place_ memory.
         | You can choose the stack or the heap. The placement can matter
         | in hot regions of code. Additionally, Rust is pretty in-your-
         | face when it comes to concurrency and sharing memory across
         | thread/task boundaries.
        
           | jcelerier wrote:
           | It kills me that RAII is considered modern c++. It's there
           | since 1983 aha, what do you think fstream and std::vector are
           | if not RAII wrappers over files or memory
        
             | bluGill wrote:
             | before unique_ptr we didn't have a good way to handle raii
             | for a lot of things. I wrote a lot of RAII wrappers for
             | various things (still do, but a lot less). Attempts like
             | auto_ptr show just how hard it is to make raii work well
             | before C++11.
             | 
             | Yes we had RAII, but it didn't work for a lot of cases
             | where we needed it.
        
             | oconnor663 wrote:
             | I think before the introduction of move semantics in C++11,
             | there were a lot of cases where you needed new and delete
             | to get basic things working. (Moving an fstream around is a
             | relevant example.) So the modern rule of "don't use new and
             | delete in application code" really wasn't practical before
             | that.
        
               | jcelerier wrote:
               | No, pretty much everything could be done with swap (like
               | moving an fstream as you say). Sure, it's a bit more
               | cumbersome, but it was still RAII.
        
           | brink wrote:
           | > Additionally, Rust is pretty in-your-face when it comes to
           | concurrency and sharing memory across thread/task boundaries.
           | 
           | Use channels whenever possible.
        
           | angelzen wrote:
           | Tangentially, I did a bit of Rust work recently. I was sadly
           | unable to find a concise credible answer to a rather
           | elementary best-practices question: How does ownership
           | interact with nested datastructures? Is it possible to build
           | a heap tree without Boxing every node explicitly?
        
             | miloignis wrote:
             | This question is a bit subtle, it depends on exactly what
             | you mean. You could make a tree using only borrow checked
             | references and the compiler would make sure that parent
             | nodes go out of scope at the same time or before the child
             | nodes they point to, but I don't think that's what you're
             | talking about.
             | 
             | In general, if it's a datastructure where you have to use
             | pointers, you'll have them Box'ed, but you would try to
             | avoid that if you can. In your example of a heap, you'd
             | want to use an array-based implementation, probably backed
             | by a growable Vec, and use indexes internally. A peek
             | function would still return a normal Rust reference to the
             | data, and the borrow checker would make sure that you don't
             | mutate the heap's backing array while that reference was
             | still in use, etc.
        
               | slaymaker1907 wrote:
               | I never thought about using a Vec for these, but that is
               | a great idea for keeping the memory management sane for
               | tree/linked lists.
               | 
               | One thing I would add that you need to be wary of
               | destructors with large pointer data structures in Rust
               | since it can easily stack overflow. When using
               | Option<Box<T>> you need to be careful to call
               | Option::take on the pointers in a loop to avoid stack
               | overflow.
        
             | steveklabnik wrote:
             | You'd do the same stuff you'd do in C++ here; allocate
             | every node explicitly, use an arena, whatever you want.
        
         | slaymaker1907 wrote:
         | While some commenters have pointed out that you still need to
         | deal with lifetimes/thinking about where stuff lives, in
         | practice you can avoid almost all of this by using Rc<Type>
         | instead of Type everywhere (or Arc in a multithreaded
         | scenario).
         | 
         | Yes Rc and equivalents have a performance overhead, but for
         | many use cases the overhead really isn't that bad since you
         | typically aren't creating tons of copies. In practice, I've
         | found one can ignore lifetimes in almost all cases even when
         | using references except when storing them in structs or
         | closures. So really you would just need to increment the Rc
         | counter for structs/closures outside of allocation/deallocation
         | which is dominated by calls to malloc/free.
        
           | throwaway894345 wrote:
           | I've tried this before and it was so laborious that I
           | regretted it. I'm not sure I saved myself any time over
           | writing "vanilla" Rust or whatever one might call the default
           | alternative. If I was really interested in writing Rust more
           | quickly, I would just clone everything rather than Rc it, but
           | in whichever case you're still moving quite a lot slower than
           | you would in Go.
        
         | sgift wrote:
         | I also was confused about that part but for another reason: The
         | whole post is basically "despite go having a GC we had to
         | manually manage the memory to make it work" and then the anti-
         | rewrite is "go does memory management for us". IMO people
         | sometimes have really weird ideas what is and isn't part of
         | managing memory.
        
         | steveklabnik wrote:
         | Yes, Rust kinda doesn't fit super cleanly into a very
         | black/white binary here. It is automatic in the sense that you
         | do not generally call malloc/free. The compiler handles this
         | for you. At the same time, you have a lot more control than you
         | do in a language with a GC, and so to some people, it feels
         | more manual.
         | 
         | It's also like, a perception thing in some sense. Imagine
         | someone writes some code. They get a compiler error. There are
         | two ways to react to this event:
         | 
         | "Wow the compiler didn't make this work, I have to think about
         | memory all the time."
         | 
         | "Ah, the compiler caught a mistake for me. Thank goodness I
         | don't have to think about this for myself."
         | 
         | Both perceptions make sense, but seem to be in complete and
         | total opposition.
        
           | throwaway894345 wrote:
           | "Manual vs automatic" is mostly just a semantic problem IMHO.
           | We could say "runtime versus compile time" to be more
           | precise, but maybe there are problems there as well. The more
           | interesting question to me is "how much time/energy do I
           | spend thinking about memory management, and is that how my
           | time is best spent?". In cases of high performance code, you
           | might spend more time fighting with the GC than you would
           | with the borrow checker to get the performance you need, but
           | for everything else the hot paths are so few and far between
           | you're most likely better off fighting with the GC 1% of the
           | time and not fighting anything the other 99%.
           | 
           | The Rust community has done laudable work in bringing down
           | the cognitive threshold of "manual / compile-time" memory
           | management, but I think we're finding out that the returns
           | are diminishing quickly and there's still quite a chasm
           | between borrow checking and GC with respect to developer
           | velocity.
        
             | steveklabnik wrote:
             | "developer velocity" is also, in some sense, a semantic
             | question. I am special, of course, but basically, if you
             | include things like "time fixing bugs that would have been
             | prevented in Rust in the first place", my velocity is
             | higher in Rust than in many GC'd languages I've used in the
             | past. It just depends on so many factors it's impossible to
             | say definitively one way or another.
        
               | tptacek wrote:
               | I have trouble believing this, at least in any
               | generalizable way. I'm comfortable in both Go and Rust at
               | this point (my Rust has gotten better since last year
               | when I was griping about it on HN), and it's simply the
               | case that I have to think more carefully about things in
               | Rust because Go takes care of them for me. It's not a
               | "think more carefully and you're rewarded with a program
               | that runs more reliably and so you make up the time in
               | debugging" thing; it's just slower to write a Rust
               | program, because the memory management is much fiddlier.
               | 
               | This seems pretty close to objective. It doesn't seem
               | like a semantic question at all. These things are all
               | "knowable" and "catalogable".
               | 
               | (I like Rust more now than I did last year; I'm not
               | dunking on it.)
        
               | steveklabnik wrote:
               | I know you're not :) I try to be explicit that I'm only
               | talking about my own experience here. I try not to write
               | about my experiences with Go because it was a _very_ long
               | time ago at this point, and I find it a bit distasteful
               | to talk about for various reasons, but we apparently have
               | quite different experiences.
               | 
               | Maybe it depends on other factors too. But in practice, I
               | basically never think about memory management. I write
               | code. The compiler sometimes complains. When it does,
               | 99.9% of the time I go "oh yeah" and then fix it. It's
               | not a significant part of my experience when writing
               | code. It does not slow me down, and the 0.1% of the time
               | when it does, it's made up for it in some other part of
               | the process.
               | 
               | I wish there was a good way to _actually_ test these
               | sorts of things.
        
               | throwaway894345 wrote:
               | This jives very well with my experience. I _like_ writing
               | Rust, but I do so well aware that I could write the same
               | thing in Go and still have quite a lot of time left-over
               | for debugging issues.
               | 
               | I can also get user feedback sooner and thus pivot my
               | implementation more quickly, which is a more subtle angle
               | that is so rarely broached in these kinds of
               | conversations.
               | 
               | The places where I think the gap between Go and Rust is
               | the smallest (due to Rust's type system) are things like
               | compilers where you have a lot of algebraic data types to
               | model--Rust's enums + pattern matching are great here.
        
               | tptacek wrote:
               | I always miss match and options (I could go either way on
               | results, which tend to devolve into a shouting match
               | between my modules with the type system badly
               | refereeing). But my general experience is, I switch from
               | writing in Rust to Go, and I immediately notice how much
               | more quickly I'm getting code into the editor. It's
               | pretty hard to miss the difference.
        
           | smoldesu wrote:
           | It's very much a confusing process. If C-styled memory
           | management is skydiving and Python is parachuting, Rust can
           | feel a bit like bungee-jumping. It's neither working for or
           | against you, but it will behave in a specific way that you
           | have to learn to work around. Your reward for getting better
           | at that system is less mental hassle overall, but it's
           | _definitely_ a strange feeling, particularly if you 're
           | already comfortable with traditional memory management.
        
         | sreque wrote:
         | I'm not a rust user, but I would argue you are still managing
         | memory manually, you're just doing a lot of it through rust's
         | type system, which can check for errors at compile time, rather
         | than through runtime APIs like the C or C++ standard library.
         | The question then becomes whether it is easier to manage memory
         | through Rust's type system versus via standard runtime APIs.
         | 
         | From what I've read, Rust memory management actually requires
         | _more_ work but provides fantastic safety guarantees. This
         | could mean that rust actually lowers productivity at first, but
         | as the complexity of the code base grows, some of that
         | productivity is restored or even supercedes C /C++ because you
         | spend no time chasing runtime memory bugs.
         | 
         | For some products or projects, the costs of shipping a security
         | flaw caused by a memory bug exploit could be high enough that a
         | drop in productivity from Rust relative to C is still more than
         | justified due to external costs that Rust mitigates.
        
         | oconnor663 wrote:
         | I think sometimes the "compiler manages memory for you" concept
         | gets overplayed a bit. It's not as complex as that description
         | makes it sound. If you understand C++ destructors, it's really
         | the same thing. Objects get destroyed when they go out of
         | scope, and any memory or other resources they own get freed.
         | The differences come up when you look at what happens when you
         | make a mistake, like holding a pointer to a freed object. (Rust
         | catches these mistakes at compile time, which does indeed
         | involve some new complexity.)
        
           | pjmlp wrote:
           | Try to implement a data structure that works across async
           | runtimes, or a couple of GUI widgets, then you will get the
           | point why some of us complain about the borrow checker, even
           | with decades of experience in C and C++.
        
         | dgb23 wrote:
         | You are still managing memory in Rust, it's just more
         | constrained, statically checked and inferred. Within those
         | constraints you have full control.
        
         | pjc50 wrote:
         | You can also kind of do your own management of memory in GC
         | languages, you just have to be extremely careful in code review
         | to spot inadvertant allocations in the hot path. A great
         | example is the "LMAX Disruptor" in Java: https://lmax-
         | exchange.github.io/disruptor/
         | 
         | The trick is to pre-allocate all your objects and buffers and
         | reuse them in a ring buffer. Similar techniques work in zero-
         | malloc embedded C environments.
        
         | bilboa wrote:
         | While you may not have to directly call malloc and free in
         | Rust, the memory management still feels very manual compared to
         | a language with GC. When I want to pass an object around I have
         | to decide whether to pass a &_, a Box<_>, Rc<_>, or
         | Rc<RefCell<_>>, or a &Rc<RefCell<_>>, etc. And then there are
         | lifetime parameters, and having to constantly be aware of
         | relative lifetimes of objects. Those are all manual decisions
         | related to memory management that you have to constantly make
         | in Rust that you wouldn't need to think about in Go or Python
         | or Java.
         | 
         | Similarly, idiomatic modern C++ rarely needs new and delete
         | calls, but I'd still say it has manual memory management.
         | 
         | I suppose it's reasonable to talk about degrees of manual-ness,
         | and say that memory management in Rust or modern C++ is less
         | manual than C, but more manual than Go/Python/Java.
        
         | mullr wrote:
         | > Isn't part of the point of Rust that you don't manage memory
         | yourself, and rather that the compiler is smart enough to
         | manage it for you?
         | 
         | For trivial cases, kind of. But once you start to do anything
         | remotely sophisticated, no. Everything you do in Rust is
         | _checked_ w.r.t. memory management, but you still need to make
         | many choices about it. All the stuff about lifetimes,
         | borrowing, etc: that 's memory management. The compiler's
         | checking it for you, but you still need to design stuff sanely,
         | with memory management (and the checking thereof) in mind. It's
         | easy to back yourself into a corner if you ignore this.
        
       | jgrant27 wrote:
       | After using Rust for a few years professionally it's my take that
       | people that really want to use it haven't had much experience
       | with it on real world projects. It just doesn't live up to the
       | hype that surrounds it.
       | 
       | The memory and CPU savings are negligible between Go and Rust in
       | practice no matter what people might claim in theory. However,
       | the side effects of making your team less productive by using
       | Rust is a much higher price to pay than just running you Go
       | service on more powerful hardware.
       | 
       | There are many other non-obvious problems with going to Rust that
       | I won't get into here but they can be quite costly and invisible
       | at first and impossible to fix later.
       | 
       | Simple is better. Stay with Go.
        
         | adamnemecek wrote:
         | Can you name some non-obvious problems?
        
         | angelzen wrote:
         | Explicitly managed memory is useful for handling buffers.
         | Everything else is peanuts anyways and could use a GC for
         | ergonomics reasons. That being said, some really prefer the
         | ergonomics of working with Result and combinators compared with
         | the endless litany "x, err = foo(); if err !== null". IMHO
         | there is still room for significant progress in this space,
         | neither Rust nor Go have hit the sweetspot yet.
        
         | IshKebab wrote:
         | Why do you say "less productive with Rust"? In my experience
         | I'm more productive with Rust because it's very strong type
         | system catches so many bugs.
        
       | srcreigh wrote:
       | > But our profile wasn't ever showing us 500GB of live data, just
       | a little bit more than 200MB in the worst cases. This suggested
       | to me that we'd done all we could with live objects.
       | 
       | Is this a typo? Weren't seeing 500 MB of live data, just a little
       | more than 200MB in the worst case?
       | 
       | EDIT: Btw, I read the entire article. It was fascinating, thank
       | you!
        
         | [deleted]
        
         | markgritter wrote:
         | Yes, that's a typo, thanks!
        
       | henning wrote:
       | > Rust has manual memory management, which means that whenever
       | we're writing code we'll have to take the time to manage memory
       | ourselves.
       | 
       | No.
        
         | arsome wrote:
         | Yeah, sounds like someone doesn't understand lifetimes and
         | RAII. Even in modern C++ the number of times you have to
         | actually think about memory management instead of lifetimes is
         | basically zero unless you have to work with old libraries.
        
           | tsimionescu wrote:
           | But thinking about lifetimes and RAII is 90% of memory
           | management.
           | 
           | Basically whether you write C, C++, or Rust, you have to
           | track ownership the same ways, the only thing that changes is
           | how much the compiler helps you with that. However, if you
           | write your program in Java, Lisp or Haskell, you simply do
           | not care about ownership for memory-only objects, and can
           | structure your program significantly differently.
           | 
           | This can have significant impact on certain types of
           | workflows, especially when it comes to shared objects. A
           | well-known example is when implementing lock-free data
           | structures based on compare-and-swap, where you need to free
           | the old copy of the structure after a successful compare-and-
           | swap; but, you can't free it since you don't know who may
           | still be reading from it. Here is an in-depth write-up from
           | Andrei Alexandrescu on the topic [0].
           | 
           | Note: I am using "object" here in the sense from C -
           | basically any piece of data that was allocated.
           | 
           | [0] http://erdani.org/publications/cuj-2004-10.pdf
        
             | bluGill wrote:
             | With modern C++ your memory checklist is two steps: put it
             | on the stack, put it in a unique_ptr on the stack. There
             | are more steps after that, but you almost never get to them
             | and wouldn't remember them if you discovered the need for
             | them (which is okay because you never get there).
        
               | tsimionescu wrote:
               | Your checklist is only covering the simplest case, direct
               | ownership of small data structures.
               | 
               | I'm not going to put a large array on the stack. I'm not
               | going to pass unique_ptr (exclusive ownership) of every
               | resource I allocate to every caller. I still need to
               | decide between passing a copy, a unique_ptr, a reference,
               | or a shared_ptr. When I design a data structure with
               | interior pointers, I need to define some ownership
               | semantics and make sure they are natural (for example, in
               | a graph that supports cycles, there is no natural notion
               | of ownership between graph nodes).
               | 
               | These are all questions that are irrelevant in a GC
               | langauge, for memory resources.
        
               | pjmlp wrote:
               | Not really irrelevant when the said GC language also does
               | value types, e.g.                  // C#
               | Span<byte> buffer = stackalloc byte[1024];
        
               | UncleEntity wrote:
               | > ... put it on the stack, put it in a unique_ptr on the
               | stack.
               | 
               | What happens when the stack frame gets destroyed but you
               | kept a reference to the data around somewhere because you
               | needed it for further compilation?
               | 
               | I, for one, am a fan of using the heap when doing the C++
               | things...
        
           | pjmlp wrote:
           | I guess something like Android Oboe, macOS DriverKit, Windows
           | Runtime C++ Template Library, or C++/WinRT could be
           | considered old libraries then.
        
           | david422 wrote:
           | Even then, just add a wrapper and off you go.
        
       | tptacek wrote:
       | The big wins in this article, in what I believe was the order of
       | impact:
       | 
       | * They do raw packet reassembly using gopacket, and gopacket
       | keeps TCP reassembly buffers that can grow without bound when you
       | miss a TCP segment. They capped the buffers, and the huge 5G
       | spikes went away.
       | 
       | * They were reading whole buffers into memory before handing them
       | off to YAML and JSON parsers. They passed readers instead.
       | 
       | * They were using a protobuf diffing library that used `reflect`
       | under the hood, which allocates. They generated their own
       | explicit object inspection thingies.
       | 
       | * They stopped compiling regexps on the fly and moved the regexps
       | to package variables. (I actually don't know if this was a
       | significant win; there might just be the three big wins.)
       | 
       | This is a great article. But none of these seem Go-specific+, or
       | even GC-specific. They're doing something really ambitious
       | (slurping packets up off the wire against busy API servers,
       | reassembling them in userland into streams, and then parsing the
       | contents of the streams). Memory usage was going to be fiddly no
       | matter what they built with. The problems they ran up against
       | seem pretty textbook.
       | 
       | Frankly I'm surprised Go acquitted itself as well as it did here.
       | 
       | + _Maybe the perils of `reflect` count as a Go thing; it 's worth
       | noting that there's folk wisdom in Go-land to avoid `reflect`
       | when possible._
        
         | jrockway wrote:
         | Agree strongly here. These are common sources of memory leaks
         | in any language, and it's very likely that rewriting this code
         | in Rust would lead to the exact same problems. (Other cases on
         | HN, like Discord's in-memory cache and Twitch's "memory
         | ballast" thing, are pretty Go specific -- the identical C
         | program wouldn't have those particular bugs. But, the Go
         | developers read these incident reports and do fix the
         | underlying causes; I think Twitch's need for the "memory
         | ballast" got fixed a few years ago, but well after the "don't
         | use Go for that" meme was popularized.)
         | 
         | Buffering is a pretty common bad habit. As programmers, we know
         | stuff is going to go wrong, and we don't want to tell the user
         | "come back later" (or in this case, undercount TCP stream
         | metrics)... we want to save the data and automatically process
         | it when we can so they don't have to. But, unfortunately it's
         | an intrinsic Law Of The Universe that if data comes in a X
         | bytes per second, and leaves at X-k bytes per second, then
         | eventually you will use all storage space in the Universe for
         | your buffer, and then you have the same problem you started
         | with. (Storage limits in mirror may be closer than they
         | appear.) Getting it into your mind that you have to apply back
         | pressure when the system is out of its design specification is
         | pretty crucial. Monitor it, alert on it, fix it, but don't
         | assume that X more bytes of RAM will solve your problem --
         | there will eventually be a bigger event that exceeds those
         | bounds.
         | 
         | Incidentally, the reason why you can make Zoom calls and use
         | SSH while you download a file is because people added software
         | to your networking stack that drops packets even though buffer
         | space in your consumer-grade router are available. That tells
         | your download to chill out so SSH and video conferencing
         | packets get a chance to be sent to the network. The people that
         | made the router had one focus -- get the highest possible
         | Speedtest score. Throughput, unfortunately, comes at the cost
         | of latency (bandwidth * buffer size for every single packet!),
         | and it's not the right decision overall.
         | 
         | I don't know where I was going with this rant but ... when your
         | system is overloaded, apply backpressure to the consumers. A
         | packet monitoring system can't do that (people wouldn't accept
         | "monitoring is overloaded, stop the main process"), but it does
         | have to give up at some point. If you don't have any more
         | memory to reassemble TCP connections, mark the stream as an
         | error and give up. If you're dumping HTTP requests into a
         | database, and the database stops responding, you'll just have
         | to tell the HTTP client at the other end "too many requests" or
         | "temporarily unavailable". To make the system more reliable,
         | keep an eye on those error metrics and do work to get them
         | down. Don't just add some buffers and cross your fingers;
         | you'll just increase latency and still be paged to fight some
         | fire when an upstream system gets slow ;)
         | 
         | Edit to add: I have a few stories here. One of them is about
         | memory limits, which I always put on any production service I
         | run. sum(memory limits) < sum(memory installed in the machine),
         | of course. One time I had Prometheus running in a k8s cluster,
         | with no memory limit. Sometimes people would run queries that
         | took a lot of RAM, and there was often slack space on the
         | machine, so nothing bad happened. Then someone's mouse driver
         | went crazy, and they opened the same Grafana tab thousands of
         | times. On a high memory query. Obviously, Prometheus used as
         | much RAM as it could, and Linux started OOM killing everything.
         | Prometheus died, was rescheduled on a healthy node, and the
         | next group of tabs killed it. Eventually, the OOM killer had
         | killed the Kubelet on every node, and no further progress could
         | be made. The moral of the story is that it would have been
         | better to serve that user 1000 "sorry, Prometheus died horribly
         | and we can't serve your request right now", which memory limits
         | would have achieved. Instead, we used up all the RAM in the
         | Universe to try to satisfy them, and still failed. (What was
         | the resolution? I think we killed the bad browser, which
         | happened to be a dashboard-displaying TV next to our desks.
         | Then kubelets restarted, and I of course updated Prometheus to
         | have a 4G memory limit. Retried 1000 tabs with an expensive
         | query, and Prometheus died and the frontend proxy served 990 of
         | the tabs an error message. Back pressure! It works! You can
         | imagine how fun this story would have been if I had cluster
         | autoscaling, though. Would have just eventually come back to a
         | $1,000,000 AWS bill and a 1000 node Kubernetes cluster ;)
        
           | Karrot_Kream wrote:
           | > it's an intrinsic Law Of The Universe that if data comes in
           | a X bytes per second, and leaves at X-k bytes per second,
           | then eventually you will use all storage space in the
           | Universe for your buffer,
           | 
           | This is known as Little's Law. Using Little's Law, you know
           | that if the average time spent in queue is more than the
           | average time it takes for a new entry to be added to the
           | queue, then your queue fills up.
        
         | kevingadd wrote:
         | Reflection APIs seem to be pretty messy and slow in every
         | runtime I've ever used, perhaps because the idea of optimizing
         | them might encourage more use. The C# reflection APIs also
         | allocate a lot.
        
           | tptacek wrote:
           | A thing you can ding Go for is that you can find yourself
           | relying on `reflect` (under the hood) more than you expect,
           | because it's how you do things like read struct tags for
           | things like JSON.
           | 
           | But that's not what the problem was here; the product they
           | were building was using `reflect` in anger. They were relying
           | on something that did magic, pulling a rabbit out of its hat
           | to automatically compare protobuf thingies. They used it on a
           | hot path. The room quickly filled with rabbit corpses. I
           | guess you can blame Go for the existence of those kinds of
           | libraries, but most perf-sensitive devs know that they're a
           | risk.
        
             | atombender wrote:
             | Reflection is also typically needed for anything that needs
             | to be generic over types. For example, if you want to write
             | a function that can traverse or transform a map or slice,
             | where the actual types aren't known at compile time. We
             | have a lot of this in our Go code at the company I work
             | for. I'm really looking forward to generics, which will
             | help us rip out a ton of reflect calls.
        
               | tptacek wrote:
               | That kind of code is generally non-idiomatic in Go. An
               | experienced Go programmer looks at something that is
               | generic over types and does something interesting and
               | instinctively asks "what gives, where are the dead
               | rabbits?".
               | 
               | I'm less excited about generics. There's a cognitive cost
               | to them, and the constraint current Go has against
               | writing type-generic code is often very useful, the same
               | way a word count limit is useful when writing a column.
               | It changes the way you write, and often for the better.
        
               | josephg wrote:
               | I'm so conflicted on that point. I've been writing a high
               | performance CRDT in rust for the last few months, and I'm
               | leaning heavily on generics. For example, one of my types
               | is a special b-tree for RLE data. (So each entry is a
               | simple range of values). The b-tree is used in about 3-4
               | different contexts, each time with a different type
               | parameter depending on what I need. Without genetics I'd
               | need to either duplicate my code or do something simpler
               | (and slower). I can imagine the same library in
               | javascript with dynamic types and I think the result
               | would be easier to read. But the resulting executable
               | would run much slower, and the code would be way more
               | error prone. (I couldn't lean on the compiler to find
               | bugs. Even TS wouldn't be rich enough.)
               | 
               | Generics definitely make code harder to write and
               | understand. But they can also be load bearing - for
               | compile time error checking, specialisation and
               | optimization. I'm not convinced it's worth giving that
               | up.
        
               | tptacek wrote:
               | If we can be at a place where reasonable people can
               | disagree about generics, I'm super happy, and think we've
               | moved the discourse forward. There are things I like
               | about generics, particularly in Rust (I've had the
               | displeasure of dealing with them in C++, too). They're
               | just not an unalloyed good thing.
        
               | pgwhalen wrote:
               | This argument is getting a little tiresome though, isn't
               | it? It isn't simply enough to call something "non-
               | idiomatic" to gloss over a deficiency. There's a
               | cognitive cost to all language features, but most other
               | general purpose statically typed programming languages
               | seem to have come to the conclusion that the benefit
               | outweighs the cost for some form of generics.
               | 
               | I am by no means a Go basher, it is one of my favorite
               | languages. But I eagerly await generics.
        
               | tptacek wrote:
               | I could have written this more clearly. The fact that
               | things that are generic over types are non-idiomatic
               | today in Go has nothing to do with whether the upcoming
               | generics feature is good or bad. They're unrelated
               | arguments.
               | 
               | The latter argument is subjective and you might easily
               | disagree. The former argument, about experienced Go
               | programmers being wary when an API is generic over types,
               | is pretty close to an objective fact; it is a true
               | statement about conventional Go code.
        
               | pgwhalen wrote:
               | That's a fair point. Knowing not to try to write generic
               | code (since you don't have the tools) is the sign of an
               | experienced Go programmer.
               | 
               | That being said, I'm curious how much kubernetes (a
               | large, famous, Go codebase) still has code that does
               | this. I used to read that it used a ton of interface{}
               | and type assertion, but maybe that narrative is out of
               | date (or never really true). I was never too familiar
               | with the codebase myself.
        
           | ComputerGuru wrote:
           | The usual C# reflection APIs that devs turn to allocate a
           | lot, but there are ways to make them almost performant by
           | (re)using delegates and expressions. There are a number of
           | good libraries to use reflection faster, as well.
        
           | aidenn0 wrote:
           | Before writing Clojure, Rich Hickey wrote FOIL[1], which used
           | sockets to communicate between common lisp and the JVM (or
           | CLR). When asked about making it in-process, Rich observed
           | that the reflection overhead on the JVM was often as large,
           | or larger, than the serialization overhead, so the gains to
           | be had were limited.
           | 
           | 1: http://foil.sourceforge.net/
        
             | hinkley wrote:
             | From what I recall, the Java team copped to the
             | intentionally slow accusation, but that started to change
             | when they decided to embrace the notion of other languages
             | besides Java running on the JVM. Unfortunately that would
             | have been shortly after Clojure was born. It took a few
             | releases for them to really improve that situation, and
             | that was still shortly before they started doing faster
             | releases.
        
         | titzer wrote:
         | > Frankly I'm surprised Go acquitted itself as well as it did
         | here.
         | 
         | As opposed to, e.g. Java, which I ranted elsewhere in the
         | thread, is a trashy mess. I programmed for over a decade in
         | Java, and yeah, it's only gotten worse over the years. They
         | would have done _even more_ custom processing and bypassing of
         | the layers underneath due to Java 's typical copy-happiness.
        
           | pvg wrote:
           | This kind of analysis and remediation would work just as well
           | in Java and is often a more rigorous and effective approach
           | than the author's somewhat Java-inspired initial idea of
           | fiddling with GC parameters.
           | 
           | One big difference is that the Java runtime design intent is
           | more in the vein of 'converting memory into performance'. On
           | HN, Ron Pressler ('pron) has written a bunch of interesting
           | stuff about that over the years
           | 
           | https://hn.algolia.com/?dateRange=all&page=0&prefix=false&qu.
           | ..
        
         | marricks wrote:
         | Yeah, and perhaps someone who knows rust well could argue some
         | things are easier to do right in rust. For example, in the
         | second bullet, pass readers could be more of the norm in
         | libraries since rust in a systems programming language. Third
         | bullet to similar point.
         | 
         | I'm not saying rust is better or they made the wrong choice,
         | sounds like C++ would let users easily make the same "wrong"
         | choices, just interesting to carry the thoughts through a bit
         | further.
        
           | Thaxll wrote:
           | io.Reader() and io.Writer() are used everywhere in Go, it's
           | really a standard practice.
           | 
           | https://tour.golang.org/methods/21
        
         | Thaxll wrote:
         | It's a question I ask often in interview, how do you upload a
         | 5GB file over the network with only 1MB of memory.
        
           | jhgb wrote:
           | > with only 1MB of memory
           | 
           | Is that total system memory?
        
         | brundolf wrote:
         | "How we avoided rewriting in Rust" feels like clickbait given
         | that the answer is "our problems were algorithmic, not
         | language-specific"
        
           | gameswithgo wrote:
           | Memory issues are amplified a bit by garbage collection
           | though, in that every pointer must be stored twice, and
           | collection will take time and evict things from cpu cache
           | etc.
           | 
           | If you were struggling with this, turning to Rust might be a
           | thing people would try, even if it wasn't fixing the first
           | order problems, and only addressing the 2nd order ones.
        
             | tptacek wrote:
             | The whole post is about how Rust turned out not to be the
             | answer to exactly this problem.
        
           | akira2501 wrote:
           | A bit yea, but it is somewhat telling that their first
           | instinct was to find a GC "knob" and twist it around until
           | they could go back to ignoring their basic architecture.
           | 
           | Go and Rust are great in that they let you write code at good
           | speed, although, I think this just highlights the well known
           | problems of over optimizing a single metric.
        
           | throwaway894345 wrote:
           | I assume it's tongue-in-cheek; because "rewrite in Rust to
           | improve performance" is such a meme, the headline is subtly
           | calling attention to the fact that this is rarely good advice
           | and certainly not the first lever an engineer should reach
           | for upon running into a performance problem.
        
             | brundolf wrote:
             | It's not the first lever an engineer should reach for
             | regardless of the languages involved. Calling out Rust
             | specifically feels like a bit of a cheap shot
        
               | pjmlp wrote:
               | To be fair, that is now the common "I rewrote X in Y"
               | theme, which followed upon the Y [?] { Ruby, Clojure,
               | Scala, Kotlin,.... } from previous years.
        
               | Zababa wrote:
               | And Go too! It's always fun to see posts from around
               | 2014/2015 complaining about how every submission to
               | Hacker News is now "I wrote X in Go", while now Go is the
               | boring stuff and Rust is the hot new thing. I wonder what
               | will be the next Rust though.
        
               | tptacek wrote:
               | BPF-verified C.
        
               | pjmlp wrote:
               | Some GC based language with dependent types.
        
               | throwaway894345 wrote:
               | It's a shot at the "just rewrite it in Rust" meme, not at
               | Rust or the Rust community.
        
             | maleldil wrote:
             | > subtly calling attention
             | 
             | That's generous. I'd call it clickbait.
        
               | wibagusto wrote:
               | Well consider all the projects titled "blah blah blah...
               | written in Rust"
               | 
               | Who gives a shit what it's written in--what does it do?
        
               | marcos100 wrote:
               | People who is interested in rust maybe want to see how it
               | was used.
               | 
               | The author could have just kept "Taming Go's Memory
               | Usage".
               | 
               | Maybe they never considered rewriting in rust. The pros
               | and cons looks like just some random arguments to add
               | rust to the title.
        
       | typical182 wrote:
       | Very nice write up.
       | 
       |  _Go's focus on simplicity means that there is only a single
       | parameter, SetGCPercent, which controls how much larger the heap
       | is than the live objects within it_.
       | 
       | FWIW, there is a new proposal from a member of the core Go team
       | to add a second GC knob in the form of a soft limit on total
       | memory:
       | 
       | https://github.com/golang/proposal/blob/master/design/48409-...
       | 
       | It includes some provisions to make sure that the application can
       | keep making progress and avoid death spirals (part of the reason
       | why it is a "soft" limit), and also includes some new GC-related
       | telemetry.
       | 
       | From the blog write up, a second GC knob with a soft limit might
       | have only been a minor help here, with the bigger wins coming
       | from the code changes they described in the blog.
        
       | option_greek wrote:
       | I have a feeling that they will end up eventually rewriting this
       | in Rust as the use case they describe is where a non GC language
       | can definitely provide more performance (beyond the case they
       | solved). APM tools usually need to be more performant to ensure
       | they add as little overhead to the actual service as possible. I
       | guess what's helping here is that this is passive monitoring
       | which allows a little lag in the system. Question relavent here
       | is will there be more issues with memory in general based on
       | their current roadmap.
        
       | rossmohax wrote:
       | Every article on Go allocations can benefit from a heap escape
       | analysis section. I was hoping to find one here, but no luck.
       | Stack allocation is a powerfull technique to reduce GC times.
        
       | CraigJPerry wrote:
       | > For our application, it would be acceptable to simply exit when
       | memory usage gets too large
       | 
       | Could you not just set a ulimit on memory usage of the process in
       | that case? (And use another process as the parent, e.g. a
       | supervisor or init, to avoid exiting the container and just
       | restart the process instead)
        
       | [deleted]
        
       | void_mint wrote:
       | Rebuilding in a different language is just trading one problem
       | set for another. Better using the tools you've already taken on
       | is a much better strategy if you don't have the money to hire a
       | whole new set of devs or a year to burn onboarding onto a new
       | language.
        
       | geodel wrote:
       | Well good for author that they were able to fix the issue.
       | 
       | However I think writing efficient code in even in managed memory
       | languages for large, heavily used service is kind of normal thing
       | and not above and beyond normal work.
        
       | favorited wrote:
       | If I was going to write a satire piece representing a typical HN
       | post, I would 100% start it with the same opening 2 sentences.
        
       | wrs wrote:
       | Buried in here are great examples of why rewrites don't help:
       | 
       | "The module that does this inference was recompiling those
       | regular expressions each time it was asked to do the work."
       | 
       | "The reason for the allocation was a buffer holding decompressed
       | data, before feeding it to a parser. ...the output of the
       | decompression could be fed directly into the parser, without any
       | extra buffer."
       | 
       | The problem here isn't that the language has GC, it's that memory
       | usage was just not considered. If you want performance, you have
       | to pay attention to allocations no matter what kind of memory
       | management your language has. And as the article demonstrates, if
       | you pay attention, you can _get_ performance no matter what kind
       | of memory management your language has.
        
         | coliveira wrote:
         | That is correct.
         | 
         | In the worst case, you can always (even on GC'd languages) pre-
         | allocate buffers and do your work without new memory requests.
         | But you need to plan for this, in the same way you'd do in a
         | language without GC.
        
         | zamadatix wrote:
         | Rewrites can definitely help but rushing into them before doing
         | these other things is going to net you a lot less gain for the
         | time.
        
         | olau wrote:
         | > The problem here isn't that the language has GC, it's that
         | memory usage was just not considered.
         | 
         | While I agree with the gist of what you're saying, I do think
         | runtimes based on the we'll-clean-it-up-some-day GC paradigm
         | makes it more important to consider memory allocation than less
         | laissez-faire paradigms (like RAII or reference counting),
         | contrary to how it's presented in the glamorous brochures.
        
           | jerf wrote:
           | Put it this way: Each of the things mentioned in that post
           | were errors that could just as easily have been made in Rust,
           | and Rust would not necessarily have helped avoid. At best you
           | can make a case for the errors being more explicit, but in my
           | personal experience even that would be weak.
           | 
           | The last error in particular, using byte buffers instead of a
           | streaming abstraction, is _pervasive_ in programming. I don
           | 't know if Rust is necessarily any worse than Go's library
           | environment for dealing with that problem but I doubt it's
           | any better. By having io.Reader in the standard library from
           | the beginning (and not because of any other particular virtue
           | of the language, IMHO) it has had one of the best ecosystems
           | for dealing with streams without having to manifest them as
           | full bytes around [1].
           | 
           | It amounts to, the root problem is that they didn't have the
           | problem they thought they have. Rust will blow the socks off
           | the competition w.r.t. memory efficiency of lots of small
           | objects, which is why it's so solid in the browser space. But
           | that's not the problem they were having. Go's just fine where
           | they seem to have ultimately ended up, stream processing
           | things with transient per-object processing. Even if you do
           | some allocation in the processing, the GC ends up not being a
           | big deal because the runs end up scanning over not much
           | memory not all that frequently. This is why Go is so popular
           | in network servers. Could Rust do better? Yes. Absolutely,
           | beyond a shadow of a doubt. But not enough to matter, in a
           | lot of cases.
           | 
           | [1]: An expansion on that thought if you like:
           | https://news.ycombinator.com/item?id=28368080
        
             | tptacek wrote:
             | I think the Rust and Go stories with buffers vs. readers is
             | pretty comparable. They both have good support for readers,
             | and to-good support for reading whole messages into slices
             | or Vec<u8>'s.
        
               | jerf wrote:
               | Good to hear. I hope it's something all new languages
               | have going forward, because like I mentioned in my
               | extended post it's almost all about setting the tone
               | correctly early in the standard library & culture, rather
               | than any sort of "language feature" Go had.
               | 
               | As mostly-a-network engineer it's a major pet peeve of
               | mine when I have to step back into some environment where
               | everything works with strings. I can just feel the memory
               | screaming.
        
           | sreque wrote:
           | More importantly, GC'ed languages tend to use at least 2x the
           | memory of un-GC'ed languages and have to deal with the
           | consequences of GC-induced pauses and generally inferior
           | native code interop. Whether that matters to you or not
           | depends on your application. No one is going to use a GC'ed
           | language in the Linux Kernel, but practically 100% of backend
           | applications are written in GC'ed languages because the
           | productivity benefits are of automatic memory management are
           | massive.
        
             | fiddlerwoaroof wrote:
             | I'm not really sure if that 2x figure is accurate. I've
             | seen charts on both sides of this and a lot here depends on
             | your programming language and the things it can optimize:
             | with Linear/Affine types, I'm fairly sure Haskell could, in
             | theory, eliminate GC deterministically from the critical
             | sections of your code-base without forcing you to adopt
             | manual memory management universally.
             | 
             | But, there's just the fact that people writing real-
             | time/near real-time systems do, in fact, choose GC
             | languages and make it work: video games are one example
             | with Minecraft and Unity being the major examples. But also
             | HFT systems: Jane Street heavily uses Ocaml and other
             | companies use Java/etc. with specialized GCs.
             | 
             | This is not even to mention the microbenchmarks that seem
             | to indicate that Common Lisp and Java can match or exceed
             | Rust for tasks like implementing lock-free hash maps and
             | various other things https://programming-language-
             | benchmarks.vercel.app/problem/s...
        
               | sreque wrote:
               | I am aware that you can hit really good latency targets
               | with GC'ed languages, like in the video game and finance
               | industry. Whenever I investigate examples, though, I find
               | the devs have to go through a ton of effort to avoid
               | memory allocations, and then I ask if using the GC'ed
               | language was even worth it in the first place?
               | 
               | I'm actually fascinated with the idea of going off-heap
               | in the hotspots of GC'ed languages to get better
               | performance. Netty, for instance, relies on off-heap
               | allocations to achieve better networking performance.
               | But, once you do so, you start incurring the
               | disadvantages of languages like C/C++, and it can get
               | complicated mixing the two styles of code.
        
               | vp8989 wrote:
               | "Whenever I investigate examples, though, I find the devs
               | have to go through a ton of effort to avoid memory
               | allocations"
               | 
               | Yep, also the median dev in a GC'ed language is simply
               | incapable of writing super efficient code in these
               | languages because they rarely have to. You would have to
               | bring in the best of the best people from those
               | communities or put your existing devs through a pretty
               | significant education process that is similar in
               | difficulty to just learning/using Rust.
               | 
               | The resulting code will be very different to what typical
               | code looks like in those languages, so the supposed
               | homogeneity benefits of just writing fast C#/Java when
               | it's needed are probably not quite true. You'd basically
               | have to keep that project staffed up with these kinds of
               | people and ensure they have very good Prod observability
               | to ensure regressions don't appear.
        
               | sreque wrote:
               | Yes, and I think one important aspect to this is the
               | necessary CI/CD changes needed to support these kinds of
               | optimizations. If your performance targets are tight
               | enough that you are making significant non-standard
               | optimizations in your GC'ed language, you're probably
               | going to want some automated performance regression
               | testing in your deployment pipeline to ensure you don't
               | ship something that falls down under load. In my
               | experience, building and maintaining those pipeline
               | components is not easy.
        
             | tsimionescu wrote:
             | I mostly agree with what you're saying, but I'll also add
             | that GC pauses are mostly a problem of yester-year unless
             | you're either managing truly enormous amounts of memory or
             | have hard real-time requirements (and even then it's
             | debatable). Modern GCs, as seen in Go, Java 11+, .NET 4.5+
             | guarantee sub-millisecond pauses on terrabyte-large heaps
             | (I believe the JS GC does as well, but I'm less sure).
        
         | sreque wrote:
         | I downvoted you at first and then changed my mind. I think I
         | would like your comment more if it were more worded like:
         | "buried in here are great examples of important optimizations
         | that did not require a rewrite". Or something like: "this
         | article does a great job of showing that you can hit many
         | reasonable performance targets while using a GC'ed language
         | like Go."
         | 
         | You can pretty much always get better performance with more
         | control over memory, and more importantly, you can dramatically
         | lower overall memory usage and avoid GC pauses, but you have to
         | weigh that against the fact that automated memory management is
         | one of the few programming language features that is basically
         | proven to give a massive developer productivity boost. In my
         | corner of the industry, everyone chooses the GC'ed languages
         | and performance isn't really a major concern most of the time.
        
           | [deleted]
        
         | xondono wrote:
         | > Buried in here are great examples of why rewrites don't help
         | 
         | That has not been my experience. Rewrites do _sometimes_ help,
         | because in a lot of codebases there's too many "pet" modules or
         | badly designed frozen interfaces.
         | 
         | Rewrites _can_ help in those situations, because there's no
         | sacred cows anymore. The issue is that a lot of people do
         | rewrites as translations, without touching structures.
        
           | laumars wrote:
           | This is where profiling helps more. Find the weak parts of
           | the code, try to optimise those. If the language proves to be
           | a barrier then you have a justification for a rewrite.
           | 
           | All too often people don't understand how to performance tune
           | software properly and instead blame other things first (eg
           | garbage collection)
        
             | bluGill wrote:
             | Most slow languages make escape to C easy for cases where
             | the language is the issue. Most fast languages make writing
             | a C APIed interface easy, so if the language is your issue
             | just rewrite the parts where that is the problem.
             | 
             | Of course eventually you get to the point where enough of
             | the code is in a fast language that writing everything in
             | the fast language to avoid the pain of language interfaces
             | is worth it.
        
               | pjmlp wrote:
               | Except when the program is actually written in C, then
               | better hold the Algorithms and Data Structures book and
               | dust it off, or Intel/AMD/ARM/... manuals.
        
               | laumars wrote:
               | And there's time when even C isn't sufficient and a
               | developer needs to resort to inlined assembly. But most
               | of the time the starting language (whatever that might
               | be) is good enough. Even here, the issue wasn't the
               | language, it was the implementation. And even where the
               | problem is the language, there will always be hot paths
               | that need hardware performant code (be that CPU, memory,
               | or sometimes other devices like disk IO) and there will
               | be other parts in most programs that need to be optimised
               | for developer performance.
               | 
               | Not everyone is writing sqlite or kernel development
               | level software. Most software projects are a trade off of
               | time vs purity.
               | 
               | That all said, backend web development is probably the
               | edge case here. But even there, that's only true if
               | you're trying to serve several thousand requests a second
               | on a monolithic site in something like CGI/Perl. _Then_
               | I'd argue there's not point fixing any hot paths and just
               | rewrite the entire thing. But even then, there's still no
               | need to jump straight to C, skipping Go, Java, C#, and
               | countless others.
        
           | silisili wrote:
           | Agreed with this 100%.
           | 
           | So many posts here over the years of examples of 'how we
           | rewrote from x to y and saw 2000% gains', where x and y are
           | languages. Such examples are 100% meaningless. Rewrites from
           | the ground up -should- always be way faster, since it's all
           | greenfield. If trying to make a language comparison, rewrite
           | the entire thing in both languages!
        
             | josephg wrote:
             | Yes absolutely. I wrote an article a couple months ago
             | which was trending here where I got a 5000x performance
             | improvement over an existing system. One of the changes I
             | made was moving to rust, and some people seemed to think
             | the takeaway was "rewriting the code in rust made it 5000x
             | faster". It wasn't that. Automerge already had a rust
             | version of their code which ran a benchmark in 5 minutes.
             | Yjs does the same benchmark in less than 1 second in
             | javascript.
             | 
             | Yjs is so fast because it makes better choices with its
             | data structures. A recent PR in automerge-rs brought the
             | same 5 minute test down to 2 seconds by changing the data
             | structure it uses.
             | 
             | Rust/C/C++ give you more tools to write high performance
             | code. But if you put everything on the heap with copies
             | everywhere, your code won't be necessarily any faster than
             | it would in JS / python / ruby. And on the flip side, you
             | can achieve very respectable performance in dynamic
             | languages with a bit of care along the hot path.
        
           | coliveira wrote:
           | This is less an argument for a rewrite than an argument for
           | redesigning parts of your codebase, which can be done much
           | more easily than a complete rewrite.
        
             | xondono wrote:
             | The tricky thing is that it's easy to end up with a result
             | that's not far off. Some modules will improve, but a lot of
             | the time these kind of bottlenecks tend to happen because
             | the performant version is not very idiomatic (feels weird),
             | it's too verbose, or it's to confusing to think through.
             | 
             | Unless you have the same team (and they learned the lesson
             | the first time), it's very likely to end up with modules
             | that perform in a similar way.
             | 
             | Sometimes changing the language makes thinking about the
             | problems easier.
        
           | bluGill wrote:
           | Last time I was in a rewrite the boss had the old software on
           | a computer next to him with the label "Product owner of
           | rewrite". He regularly when asked how to do something looked
           | at what that did.
        
           | hinkley wrote:
           | I would argue that the rewrites help when the information
           | architecture for the original code is proven to be wrong,
           | _and_ there is either no way to refactor the old code to the
           | new model, or employee turnover has resulted in nobody having
           | an emotional attachment to the old code.
           | 
           | That said, to slot in a new implementation you often have to
           | make the external API very similar to the old one, which can
           | complicate making the improvements you're after.
        
             | bluGill wrote:
             | > there is either no way to refactor the old code to the
             | new model
             | 
             | That doesn't happen. Write facades as needed. Even if they
             | are slower than everything else write the facades so you
             | can keep in production all along.
        
               | hinkley wrote:
               | If you get the object ownership and the internal state
               | model wrong (information architecture) facades don't help
               | you.
               | 
               | You can't put an idempotent or pure functional wrapper
               | around a design that isn't re-entrant and expect anything
               | good to come from it. IF you get it to work, it'll be dog
               | slow.
        
           | wrs wrote:
           | Quite true, a rewrite can help if it is also a "rethink". But
           | you don't have to switch languages to get that effect--in
           | fact you'll probably do _better_ if you don 't throw a new
           | language/library into the mix.
           | 
           | My point was that, contrary to what is apparently a common
           | impulse, rewriting the same thing in a different language
           | while maintaining the lack of attention to performance
           | considerations that was present in the first version isn't
           | going to help much.
        
         | jjoonathan wrote:
         | Right, but GC encourages you to not think about memory at all
         | until the program starts tipping over and fixing the underlying
         | cause of the leak now requires an architecture change because
         | the "we hold onto everything" assumption got baked into the
         | structure in 2 places that you know about and 5 that you don't.
         | 
         | I don't miss the rote parts of manual memory management, but it
         | had the enormously beneficial side effect of making people
         | consider object lifetimes upfront (to keep the retain graph
         | acyclic) and cultivate occasional familiarity with leak
         | tracking tools. Problematic patterns like the undo queue or
         | query correlator that accidentally leak everything tended to
         | become obvious when writing the code, rather than while running
         | it. These days, I keep seeing those same memory management
         | anti-patterns show up when I ask interviewees to tell a
         | debugging war story. Sometimes I even see otherwise capable
         | devs shooting in the dark and missing when it comes to the
         | "what's eating RAM" problem.
         | 
         | I feel like GC in long-form program development substitutes a
         | small problem for a big one. Short-form programming can get
         | away with just leaking everything, which is what GC does
         | anyway, so I'm not sure there's any benefit there either.
         | 
         | tl;dr: get off my lawn.
        
           | titzer wrote:
           | GC will not fix trashy programming. The problem is that many
           | GC'd languages have adopted a style guide that commits to a
           | lot of unnecessary allocations. For example, in Java, you
           | can't parse an integer out of the middle of a string without
           | allocating in-between. Ditto with lots of other common
           | operations. Java has oodles of trashy choices. With auto-
           | boxing, allocations are hidden. Without reified (let's say,
           | type-specialized) generics, all the collection classes carry
           | extra overhead for boxing values.
           | 
           | I write almost all of my code in Virgil these days. It is
           | fully garbage-collected but nothing forces you into a trashy
           | style. E.g. I use (and reuse) StringBuilders, DataReaders,
           | and TextReaders that don't create unnecessary intermediate
           | garbage. It makes a big difference.
           | 
           | Sometimes avoiding allocation means reusing a data structure
           | and "resetting" or clearing its internal state to be empty.
           | This works if you are careful about it. It's a nightmare if
           | you are _not_ careful about it.
           | 
           | I'm not going back to manual memory management, and I don't
           | want to think about ownership. So GC.
           | 
           | edit: Java also highly discourages reimplementing common JDK
           | functionality, but I've found building a customized
           | datastructure that fits exactly my needs (e.g. an intrusive
           | doubly-linked list) can work wonders for performance.
        
             | jjoonathan wrote:
             | > many GC'd languages have adopted a style guide that
             | commits to a lot of unnecessary allocations.
             | 
             | Oh, that too. I forgot to rant about that.
             | 
             | > Virgil
             | 
             | Unfortunately I'd rather live with a crummy language that
             | has strong ecosystem, tooling, and developer availability,
             | so I'll never really know. It does sound nice, though.
        
             | pjmlp wrote:
             | Yeah, but that was one of Java's 1.0 mistakes, that
             | thankfully Go, .NET, D, Swift, among others, did not make.
             | 
             | Now lets see if Valhalla actually happens.
        
           | josephg wrote:
           | > Right, but GC encourages you to not think about memory at
           | all
           | 
           | I've come to a new obvious realisation with this sort of
           | thing recently: if you care about some metric, make a test
           | for it early and run it often.
           | 
           | If you care about correctness, grow unit tests and run them
           | at least every commit.
           | 
           | If you care about performance, write a benchmark and run it
           | often. You'll start noticing what makes performance improve
           | and regress, which over time improves your instincts. And
           | you'll start finding it upsetting when a small change drops
           | performance by a few percent.
           | 
           | If you care about memory usage, do the same thing. Make a
           | standard test suite and measure it regularly. Ideally write
           | the test as early as possible in the development process.
           | Doing things in a sloppy way will start feeling upsetting
           | when it makes the metric get worse.
           | 
           | I find when I have a clear metric, it always feels great when
           | I can make the numbers improve. And that in turn makes it
           | really effortless bring my attention to performance work.
        
           | tptacek wrote:
           | Plenty of C programs do the equivalent of ioutil.ReadAll;
           | it's not a GC thing.
        
             | jjoonathan wrote:
             | "Leak everything because we can get away with it here" is a
             | fine memory management strategy. "Why does my program keep
             | getting killed?" isn't.
        
               | tptacek wrote:
               | This has nothing to do with leaking (nothing "leaked";
               | it's a garbage-collected runtime). It's about memory
               | pressure, which, I promise you, is a very real perf
               | problem in C programs, and why we memory profile them.
               | The difference between incremental and one-shot reads is
               | not a GC vs. non-GC thing.
        
       ___________________________________________________________________
       (page generated 2021-09-21 23:00 UTC)