Discipline Doesn’t Scale
sicpers.info
sicpers.info
The encouraging thing today is that we're kind of converging on good practices in language design.
- You really want to declare types for function parameters. The compiler needs that information for checking, and the programmer, and the maintenance programmers who follow behind, need that information so they know how to call the thing. On the other hand, you can usually infer the result types of expressions by a simple forward process. So C++ gained "auto", Rust and Go started off with that arrangement, Javascript got TypeScript, and Python is getting there via a side trip through unchecked type declarations.
- The three big questions in C: "How big is it?", "Who can delete it?", and "What locks it?" C gives no help with any of those. C++ now tries to address the first two. All the garbage-collected languages address the first two. Rust addresses all three.
- The idea that locks should be connected by syntax with the data they are locking took far too long to become accepted. Partly because the Ada rendezvous and the Java synchronized object didn't work well. Even Go doesn't do that very well. But language design seems to be coming around. When and if thread-type concurrency comes to Javascript, the webcrap crowd will create race conditions everywhere without automation in locking.
I view move semantics as not a memory safety thing at all (in fact, use-after-move can be exactly as ugly as use-after-delete), and smart pointers as a “let’s just make things marginally better than raw raw new/delete” thing. Smart pointers in C++ can still be null, after all.
Calling C++ not serious about seems a reasonable take to me. Of course Rust didn’t invent all of its solutions from scratch.
I feel like you just defeated your point; those are bolted on instead of a natural part of the language.
(a) Smart pointers + static analysis + sanitizers make C++ pretty usable
(b) C++ usability requires brittle, half-baked add-ons such as smart pointers and non-deterministic/heuristic/unreliable/NP-hard development tools such as static analysis + sanitizers.
My personal opinions on C++ are not very positive, but it's a language I literally grew with. My experience is that C++ code very gradually became more stable and less buggy. The thing is, some of that is just improved discipline, some of it is static analysis, etc. But a lot of it, is genuinely C++0x/C++11 and features that built on top of this.
So the idea that Rust showed up and suddenly C++ cared about safety? I don't really see it. I think C++ developers started caring about safety and then Rust showed up because of that.
P.S.: While the borrow checker and ownership semantics is really cool, I think a programming language's ecosystem and the 'best practices' it lends itself well to have a greater impact that people completely miss. Rust, Go, Zig are all clearly trying to foster a culture of writing more correct and complete programs. Error handling doesn't just feel like a couple control flow mechanisms you can use, but a responsibility that you have. Modern C++ is getting better probably more because of this than any committee; although I really wish the Expected type would've gone somewhere, since I feel Rust's `Result<...>` type and Go's multiple-return with `error` are being proven right as ways to handle errors robustly even with their caveats. (I've heard Zig's error handling is really cool too, but I haven't had a chance to write a serious Zig program. I'll get to it soon.)
Note that move-destructors were also considered for C++11, and were not added for lack of time / implementation experience . And this is really too bad, because they are at least as important as move constructors if not more so; I've yet to encounter a case where you actually want to use a moved-from object after the move.
- C++ code where some method calls std::shared_from_this(), and that method ends up being called indirectly during the object's construction, leading to a 0-pointer dereference [1]
- accidentally creating two shared-pointers to the same object leading to an eventual double-free (that may just silently corrupt the heap while the program continues running)
- undetectable cyclic shared_ptr<> references somewhere deeply hidden in the code causing a memory leak
Modern C++ feels like a case of "but we can do that in C++, too" syndrome. Stuff that "just works" in Java or Lisp, can now be done in C++, too, however in C++ it will only work for 95% of all use cases, and break in a spectacular manner for the remaining 5%.
E.g. think about the template meta-programming madness (and efficiency WRT compile-time) and compare that with what Lisp offers (see also [2]).
[1] https://stackoverflow.com/questions/31924396/why-shared-from... standpoint
[2] http://people.cs.uchicago.edu/~jacobm/pubs/templates.html
OTOH, C++ needs the ability to support old code bases because of its age. Of course a new language can forego this.
As for cycles, for GC language using ref-counting under the hood, the same problem applies. I've also seen memory leak from accidental keeping of pointers in fully GC languages. I've seen it happens in Java multiple times. Of course, two wrongs don't make right, but in this case GC is only solving one case.
But the more fundamental issue at play is that pointer cycles usually means a lack of up-front design and thought about how a program will work. As such, it is bad, but only a symptom of a lack of proper initial design which can manifest itself in all kind of places.
auto self = shared_from_this();
document.onclick([self](){self->doit(); } );
I'm not sure there is a way to express that in C++ without use of enable_shared_from_this.It is very easy to accidentally run this kind of code from a constructor where shared_from_this() is a 0-ptr.
make_shared also only helps in 95% of cases. E.g. it does not allow specifying a custom deleter, which is a use-case that is allowed by the shared_ptr<> constructor. It also won't fix any of the problems WRT shared_from_this() being called in the constructor.
I think C++ could really use what Rust calls generations. Figure out the obviously bad ideas and deprecated them over 10 year cycles.
That would allow the language to shrink again, I don't think it ever did.
Even things like integer promotion rules or "trivial" things like "deprecate NULL and 0 for nullptr" are likely untouchable with any edition mechanism for C++ that doesn't end up Breaking the World.
Regardless - C++ will effectively has some deprecation, at least of the standard library, with the introduction of modules instead of included headers in C++20.
Everybody and their uncle had a lot of ideas about how they would fix "C++". Eventually all those contradicting ideas got merged into the C++ standard.
:)
In big multi-core systems, I model the handful of locks by the worst-case access frequency for a core to understand contention. In practice, this is often on the order of 100ms, which means the locks are effectively contention-free and the fast path almost always wins.
"Lock free" data structures are tied very closely to the data being locked, and depend heavily on certain CPU operations being atomic. This is more of an issue with ARM than with x86, because ARM needs more fence instructions. Really, "lock free" programming is just locking critical sections with lower level hardware primitives.
The opposite extreme is POSIX locks. Locks are the OS's problem, and nothing in the code says what's covered by the lock. That was once standard practice and is now recognized as being bad.
Can fence instructions really be seen as locking mechanism? While a lock generally is bound to some data, memory fences are bound to relationships between data or a piece of code. I don't know if Rust or any other language can help you with that.
Isn't the actual bad practice to expose locks at all? If the locking is done implicitly in API calls, one doesn't need to ask "What locks it".
See also, eg, the LOCK prefix on x86 instructions.
(disclaimer: I only have approximate knowledge of anything, I'm not an expert)
I have to disagree, as the more interesting lock free algorithms use atomic operations that can fail. Yes, a compare-and-swap is like having a critical section on modifying that particular address. But a compare-and-swap can fail if what is currently there is unexpected. The result of that failure generally means redoing a bunch of work, rather than just trying again. Interesting lock free algorithms tend to have the structure:
1. Read some data.
2. Perform computations on that data.
3. Trying to commit the result of that computation. If someone else committed data to the same location after you read it in step 1, go back to step 1.
That's less like a "critical section," and more like a transaction. For lock free algorithms that just rely on atomic operations that cannot fail (such as an atomic increment), then yes, those are just like having a lower-level critical section. (And, on some architectures, that's exactly what they are.)
Compare and swap is interesting. It's a tiny transaction. It can even operate across networks - FireWire had hardware compare and swap support. Compare and swap with retry may have to wait and retry, which is effectively waiting for someone else to stop using a resource you want. So it's a form of a critical section lock.
(Also, not all architecture use hardware mechanisms to enforce actual mutual exclusion. For example, Power does not, as it uses load-linked, stored-conditional: https://en.wikipedia.org/wiki/Load-link/store-conditional. On Power, even things like atomic increments are implemented with LL/SC. Architectures basically have to decide: will they implement atomics through some kind of a lock mechanism, or some kind of LL/SC mechanism.)
What lock-free means in this case is that an OS level thread freezing doesn't prevent global progress. That's true on the abstraction level of typical programming, but grandparent is very much correct:
> Really, "lock free" programming is just locking critical sections with lower level hardware primitives.
That is incorrect. Properly designed and implemented mutexes (like in the parking_lot crate [1]) are extremely fast in the uncontented case, and need just one byte of overhead (and no heap-allocated memory) per mutex. In the contented case, a lock-free algorithm still has to deal with arbitration between the different threads, so it's typically not faster than a lock-based approach. Lock-free algorithms do have certain advantages (like deadlock-freedom), but performance is typically not one of them.
> Locking is a [...] scalability destroying operation
Also incorrect. If you have N threads trying to append to a single queue, then you have a scalability problem whether you're using locked or lock-free data structures. The solution in such a case is fine-grained concurrency and a different approach, not lock-free algorithms.
A common train of thought seems to be "locks are slow, so lock free must be fast", while lock-free says nothing about performance -- only about forward progress.
The question is "What prevents me from making changes to this variable without breaking everything".
So the point is not "What locks guard this data" the point is, how do I determine whether I can currently modify (or even read) this data. This allows the compiler to check that, indeed, you never use the data in a way that would break things. Hence your compiler can then give guarantees about thread-safety.
Now it seems likely that, when compilers do this and you write wrong code, your conclusion might be "I need a mutex lock for this", or if performance matters, "I need to rewrite this to allow performant lockless code. But the main question remains "Can I currently modify this data without issues".
On top of that, locking is faster than most lock-free strategies unless you deal with high contention or deterimistic guarantees (90% of the latter is solved with bounded fifos).
That's a bad example: GPUs are all about shared memory, concurrently accessed by many threads. It's up to the programmer to make sure that there are no read/write or write/write data races.
Ideally, any shared data is immutable (and modifying it would then need to be done in a coordinated synchronised fashion) while most mutable state is only ever owned by one task/thread, with ownership explicitly passed between them as others need access, unique_ptr style. Reality doesn't always match with the ideal though.
For many applications, this is the way I prefer to structure code these days. If you know that the only part of your program that will touch certain state directly is always running in a single thread, a lot of problems just go away, because whatever you do in response to any given message or other signal is effectively an atomic transaction, regardless of any concurrency or asynchronous behaviour you have going on elsewhere in your system. You still need to figure out how your message passing or other signalling infrastructure is going to work and in particular what will block and what won’t, so it doesn’t answer all of the usual questions with concurrency, but it’s a widely useful strategy and it goes a long way toward keeping things manageable.
So I'm quite curious, what problems with it you were referring to?
Rendezvous Facilities: Concurrent C and the Ada Language - https://www.computer.org/csdl/journal/ts/1988/11/e1546/13rRU...
> The concurrent programming facilities in both Concurrent C and the Ada language are based on the rendezvous concept. Although these facilities are similar, there are substantial differences.
> Facilities in Concurrent C were designed keeping in perspective the concurrent programming facilities in the Ada language and their limitations. Concurrent C facilities have also been modified as a result of experience with its initial implementations.
> The authors compare the concurrent programming facilities in Concurrent C and Ada and show that it is easier to write a variety of concurrent programs in Concurrent C than in Ada.
Making the locks public, I sort of get (like making any field public) -- you have multiple locks, you need to ensure they are accessed in a certain order and if you make them public, you cannot enforce that order.
Instance.this is implicitly public because something else could have a reference to instance.
synchronize(this){}
that will lock with the following if its inside instance. synchronize(instance){}If you have a function in a standard library, types are obviously helpful, albeit it can be convoluted if they are templated.
On the other end of the scale, if you have a little helper function used to reduce duplication/increase readability in another function, explicit typing easily becomes busywork. This is where duck typing/templates shines.
I once sent a suggestion to the C++ committee people that they allowed easy templating of function parameters (something like "auto foo(auto bar)"), but I don't think people who are not used to duck typing appreciate how much it helps in the grunt work inside modules. At least, I didn't myself.
Similarly, you can use auto as a return type if the function implementation is immediately given and the compiler with infer the return type. The new starship operator (<=>) typical usage is pretty much declaring it with an auto return type and "= default" implementation.
Possibly unpopular opinion: I am wary of relying on type inference for return types, other than for a function defined locally where you can easily see the implementation at the same time as the code calling it. Code like
auto x = calculate_some_complicated_thing()
...
do_something_with(x)
has a similar obscurity problem to code like do_something_impressive(true, false, false, true)
in that to understand what the code is doing at this level, you have to read the interface documentation for the function you’re calling anyway, and then you’re hoping that you get everything in the right place because your tools might not be able to warn you if you don’t.Mutability is very different in C++ though - const is quite a complex topic, and isn't as simple as "this variable binding is/isn't mutable".
Using "scale" is also misleading, because to use again your example C++ is a mainstream programming language and one of the most popular in the world. It towers above Rust by any thinkable metric related to scale - popularity, projects, developers, libraries, etc.
I do agree that discipline isn't enough... in the sense that one can't rely on discipline to achieve safety. This is a well known principle of safety engineering, which applies to all systems, not just SW or SW/HW systems. Discipline remains nevertheless essential, because it's one of the main things influencing the "human factor".
Perhaps there are two camps here: (1) Reduce discipline by making developers no longer have to consider certain situations (garbage collection fits here) and (2) Reduce discipline by shifting that to the compiler (types fit in here)
My experience is completely different. I programmed a bit in rust recently. Nothing system level. Quite opposite.
I had multiple occasions when after succeeding to implement some part in Rust I thought to myself "wow, if I tried to do that in C++ I would have fallen in so many pit traps by now, that rust helped me avoid by restricting me in some ways".
And I'm very new to rust and I was just coding blindly just by powering through different approaches till one of those compiled.
"I can't use borrow to keep that? What's a borrow anyways? Let's just cram it in Rc whatever it is. I can't return this? Fiiine! I'll create it outside and pass it in."
And in the end it worked perfectly. No weird bugs to find. I felt like a caveman trying to bang rock into shape. And at no point rock cracked, split and or buried me under the rubble.
This is exactly what makes it so that the programmer doesn't have to be disciplined. The Rust compiler will correct the lazy Rust programmer, while the C++ compiler will blithely do its best.
And in your view, are the bases flawed because they are low level?
"Which is what? Still to be created, or does it exist?" - several different 'initial stakes'/bases in the space of programming languages exist/were made/can be created than the family of languages mentioned in OP. Exploration from those might yield better optima. The widespread adoption of the current industry 'standard' languages seems an accident of history, not guided by quality of language.
"Are the bases flawed because they are low level?" - yes. A significant part of the computing field is about abstractions. Improving the level of the technology is one of the arguments the article here makes too. Besides, it's the implicit admission of everyone who doesn't advocate/write assembly.
This sounds like it’s really a treatise on “learning to build a house by making your own nails.”
Of course that won’t scale. Especially if most houses, these days, are prefab, and don’t use too many nails.
But learning how to stay out of flood plains, and selecting good prefab sources, is vital to being a builder. The first one is a fundamental discipline, that also applied to “make your own nails” building, while the second is unique to the new way we do things.
I like to write code that has a future. I want people to be able to take it over, so I use header documentation, and a consistent coding style.
I originally learned Machine Code (I started as an EE). I’ve written embedded operating systems and drivers.
A lot of what I learned, doesn’t apply to writing high-level application code in Swift (what I do nowadays), but it did give me the discipline to power through the “boring” stuff that is vital to writing good, maintainable, extensible code.
I think maybe there is a confusion between “discipline” (the type that manifests as patience, consistency, and craftsmanship), and “experienced inflexibility,” which is not always applicable (and sometimes, downright destructive, like old-fashioned C++ programmers refusing to use smart pointers).
I was just thinking about the difference between “good,” and “excellent.”
If we look at the difference between a Henkels kitchen knife, and a Japanese Takayuki knife, we are talking exponentially divergent costs. The Takayuki is definitely a lot better than the Henkels, but is it twenty times as good, as the price difference might suggest?
But Henkels makes far better knives than most of the disposable crap out there. It costs more; usually two or three times as much as a “standard quality” knife. Worth it, though.
That’s sort of the “sweet spot” I aim for, and we don’t get there, without discipline, consistency, and patience.
I will say that poor folks act a lot like pros. They take care of their tools.
I know people that use Takayukis as bottle-openers. They have so much money that they don't care.
You have to set a boundary for "good enough" somewhere, else you end up stuck in analysis paralysis or whatever a catchy term is.
That said, today's development tooling helps you have a baseline of quality pretty quickly; languages that do not have memory issues, compilers and typed languages, etc.
I think there is some selection bias here. The majority of particiapants in HN discussion generally people who work for startups and people who work with web technologies. The Silicon Valley culture might be focused around time to market, but there is a large (mostly unheard) group of people who exist in a very different culture. For example: C is still an incredibly popular language (for better or worse), but on HN you won't hear much about it outside the Linux Kernel.
A lot of this enterprise software development suffers from some very different problems (the time to market thing brings benefits in terms of making productivity important) but also has benefits in terms of thinking about investments long term (when done well).
For instance, the 60s and 70s had a lot of great music come out of it, but have you ever gone down a chart of the hits from decades ago and listened to the lesser known artists? Lots of it is bad and utterly forgettable.
Same goes with a lot of other fields. We only remember the best and forget about the rest.
Software is one of the few areas where we can easily and rapidly change our tools and the medium we work with. To be professional in this field you must use that ability to systematically remove issues where possible.
To fall back on the mindset that being a good engineer means eschewing those safeguards and relying on discipline is more about image that outcome.
One of the most important things about being a software engineer, in my mind, is the ability to be flexible (I won't say "agile," as that word seems to be pretty much worthless, these days).
One of the disciplines that I've developed, over the years, is writing flexible code; Code that has a future.
One of my favorite text editors is BBEdit[0]. I've been using it since the 1990s. During that time, I've tried others that have been raved about, like Sublime, TextMate and VSCode. I've liked them, but always ended up going back to BBEdit, which has been doing a great job of adapting to the new operating systems and workstyles.
The fact that the app is nearly thirty years old, and started off with a bus factor of 1, says that the original author applied discipline and a "future-facing" design ethos, back in 1992. I can respect that.
I also use Adobe Photoshop, which I first met before Adobe brought it, in the late 1980s. That seems to have grown well.
Yes, it would appear there is an anti-pattern of programmers that feel like going lower level for certain things matters. It doesn’t always. Programming computers has multiple levels of abstraction for solving different problems.
But there’s also this idea that the magical compiler does good enough abd there’s no reason to challenge the assembler. But there are programming problems where that is in fact essential.
It’s not a kissing contest. Writing great asm or C is as challenging as writing great css. But to be sure, knowing what your code actually requires hardware to do is really valuable. It matters and we don’t have abstraction in many cases to abstract that away totally.
It doesn’t hurt to have a good idea on how the machine works.
I would argue that writing great asm that is more efficient than your idiomatic C code is going to be a very very challenging task on modern non-embedded CPUs. The compiler definitely knows more than I do about how the internal architecture works and which instructions to reorder to prevent stalls etc
SIMD intrinsics are not idiomatic C, they are idiomatic assembly.
Here's an example when they improved by a factor of 3.6: https://stackoverflow.com/a/63759887/126995 That's not an unusual outcome.
I've seen several examples of the compiler being brilliant until suddenly a simple change makes it not brilliant at all anymore. Compilers are just tools.
Winforms got a lot of hate for some reason I don't understand, but designing with it in Visual Studio is the easiest, best, most sane layout/designing experience I've ever had. It makes me wish the web was more like that.
Citation needed. I've seen people claiming this for years, and I've yet to see a single case where handwritten assembler actually did better than spending the same amount of effort on speeding up the compiled program (e.g. taking 5 minutes to actually set the right target architecture).
> Writing great asm or C is as challenging as writing great css. But to be sure, knowing what your code actually requires hardware to do is really valuable. It matters and we don’t have abstraction in many cases to abstract that away totally.
> It doesn’t hurt to have a good idea on how the machine works.
On modern hardware both C and ASM are distractions. If you actually want to understand the performance you need to look at cache misses and mispredicted branches; reading through C or an ASM listing will only serve to mislead you. Strangely enough, the crowd that wants you to learn C seem to be the same people who are vehemently against using the kind of profiling tools that will actually tell you what's going on.
Take some time looking at established open source projects where IBM has heavily invested in optimizing for the POWER architecture. I'm thinking things like glibc, gmp, Golang, openssl, ... The results they get from their hand rolled assembly far exceeds what gcc/llvm spit out. (At least, it did 2-3 years ago.)
Back when the Linux Technology Center (LTC) was a thing, I was fortunate enough to meet many of the individuals working on these projects. They are all wizards. Brilliant.
One of them retired recently and went on to start pveclib to make some of these optimizations in number crunching more accessible to others: https://github.com/open-power-sdk/pveclib
I was once in his office asking about assembly instructions for SHA-3 (a project that sadly didn't go very far) and he could quote useful scalar/vector instructions to me and their timing/latency stats faster than I could find them in the manual.
C++ with intrinsics can be very close to assembly. Compilers allocate register and do their magic with the rest of the code, but the real workload translates into assembly almost line-to-instruction. Takes longer than 5 minutes to do, these instruction sets are less than straightforward, but I think 80% of real-world performance sensitive compute-bound code is written like that (the other 20% being assembly).
> that will actually tell you what's going on
When I work on performance critical pieces, I already know what's going on. I design my code from the start to minimize cache misses and branches. The profiling tool I find most useful is sampling profiler. Then I either read assembly and try to do better, or rework higher-level logic or data structures.
The C versions were usually slower - sometimes a lot slower. They knew all the optimizations the compiler was doing and tweaked as much as they could.
In general, though, you're right. I'm particularly annoyed at universities that insist on making their students do all their microcontroller labs in assembly. When I took the course, the professor made us do only two assignments in assembly, and the rest in C. He said it had been over a decade since he had encountered a real world microcontroller problem where one needed the assembly - and he said this some decades ago.
I don't do microcontroller stuff for a living, but anecdotally, the only people I've met who insist one must program in assembly for microcontroller problems do not do it for a living.
The times I have found it necessary to write assembly (as opposed to just reading compiler output) in the past few years of microcontroller programming seem to fall into three main categories:
1. The vendor-provided startup code is broken and it needs to get fixed. Usually this is indeed 100% written in C for Cortex-M cores, but some vendors get... creative. Vendor code is special. Vendor code has always been special.
2. The compiler is too stupid to use a particular processor feature. This is usually related to performance, and in particular to the math and DSP instructions being too complicated for the poor compiler to understand. This is relatively rare, but when it's in a hot area, a single intrinsic call (sometimes you have to write the intrinsic...) can make a huge difference.
3. You need to debug something hairy and low-level hacks can help you do that. Usually this kind of debug solution only occurs to people who already have good low-level intuition; everyone else just finds a different way to solve the problem, which ends up working out just fine either way.
Citation needed? Last I checked, Graham was working on a C64 Smalltalk80 VM as a side project, so I presume he's not completely ignorant of low-level development. And statements like "Your computer is not a fast PDP11" are aimed directly at low level development.
The thing is, sometimes you need to know how those lower layers work in order to make that judgement. If you’re writing networking code in JavaScript and don’t know what sockets are, you’re going to have a bad time sooner or later.
The point of the article is to point out that languages, frameworks, or apis that expect a level of discipline to write well are inferior to ones that don't expect discipline out of its coders.
The takeaway shouldn't be - I as a programmer shouldn't learn how the machine works.
The takeaway should be - if I'm designing an API/Language/framework, I should figure out how much I'm relying on an engineer's discipline to make the code great (performant, robust, secure, etc). If I can reduce that reliance, the API/Language/Framework becomes better.
Requiring discipline is not superior or inferior, but doesn’t scale. It’s then your to decide how much it matters: do you have/need to only focus on a small group or are you tools targeted at a wide audience or you want it to widely succeed ?
The answers to that question won’t be the same for everyone.
I think this is what the parent comment that you responded to is saying is totally bogus.
A good example is Python. In my company, the teams using Python run circles around the teams using C++ and Java. The Python teams ship higher quality stuff that is both more reliable (it turns out that lack of a compiler for various static checks not only doesn’t make anything more reliable, but also slows things down unreasonably on large code bases), and easier to modify and adapt to changing requirements.
The religion of the time was to work on things that increased developer productivity time. Performance be damned. It flowed naturally at the time. Computers were (and are) fast enough to create these web apps! We just need to get more features (and better looking ones!) out the door!
And now here I am sitting on the other side, looking at all my peers work and thinking "slow down, everyone. We've made too much stuff. We need to organize and catalogue and refine and optimize"
It's like culture is a Tick/Tock cycle. This guy was a Tock - a refinement of existing technology. I was a Tick - an explosion of new technology.
And now (I believe) we are witnessing another Tock - a refinement, a culling of the weak, a centralization.
I love reading the historical programmers account. As I near 30 I realize, that I will become that, a history lesson for the fresh minds of the era.
I think I have lots to teach you. I need only to refine, cull, centralize, and catalogue my thoughts. Piece them into theories of abstraction, class hierarchies of ideas, if you will.
For I experienced the polar opposite of this. I experienced the explosion.
And yet still, I agree.
Discipline doesn't scale. This encoding of discipline into system. I agree with it.
That defines a large part of my job - to take the "best practise" and encode it into the day-to-day.
This concept: "discipline doesn't scale" is fundamental to our history. It seems trivially true.
This prose is excellent, this history lesson is important and valued, and the fable of this story rings true.
If a recession did happen and orgs started cost cutting, I imagine that R&D would drop somewhat, and you would see fewer new projects and toolkits perhaps? Even then its hard to see the pace of innovation (for lack of a better word) slowing.
It seems to me that technology is now in the stage, where its everywhere. Every industry that is quick to adopt new software gains a competitive advantage over its peers, in so many industries. This unreasonable effectiveness of software seems to feed itself.
I would love to see studies that measure/quantify the blooming and culling of technologies so we could talk about these things with more than just anecdata.
What grieves me is lack of inquisitive ness and communitcation. If we're not deepening our understanding of how things work, and working together, then things get wobbly.
True story: delimited text data ingest file. Guy wrote a C language file to hack the upper byte off the characters, and then write them back to disk. At a glance we think "C! This was a performance thing." Except laboriously writing each character after hacking it in twain and discarding the upper byte isn't a performance hack. It's a "didn't RTFM on the python open() command and try the encoding argument, so we did a C hack" move. Encoding="utf-16" worked great.
I'd take a victory lap, except for every time I'm clever, there are two where I look an ass.
Discipline may not scale, but teamwork and inquisitiveness paper over a multitude of shortcomings.
In the absence of deadlines or other external pressure, I sometimes write beautiful, uselessly incomplete code. With mild deadline pressure, I do all right. In a toxic, death march environment, I can see how teamwork and inquisitiveness might drain away and someone would write the terrible code you just described.
(See also: fundamental attribution error. I wrote the bad code because of the death march. He wrote the bad code because he's intellectually lazy.)
As senior developer, I have to go in and stop them repeatedly and make them rework it. Up until now, I've told them why it's wrong and that basically does all their thinking for them. Now I'm wondering if I need to drag that step out and make them come up with the solution instead.
The problem I've been debating lately is how to instill that inquisitiveness in my junior and mid-level devs when they lack it. (I'd been saying they don't think holistically, but I think inquisitiveness is a good work for a trait that will lead to that.)
I'm thinking maybe I change my code reviews to have some video chat time where I lead them through the problem instead of telling them.
While I agree that having nice and easier thing is nice, and easier, performance is always relevant, if not for pride or principle, at least think about the environment, computer processors and memory chips consume electricity, and are really inefficient.. How many percentage of the consumed energy turns into the desired product (computation)? Les than 1%, a CPU using 100 watt is pretty much a 100 watt room heater with a side-effect of also performing some computation..
That's a _WAY worse fuel efficient than a Model T, and yet we treat computational power and memory capacity as infinitely cheap.
In the dawn of computing, machines were far less common, much more expensive, and society was willing to pay programmers boatloads of money to write optimized code because that was cheaper.
The hardware engineers did an excellent job, they reduced costs of computing by a lot. Society is not willing to pay programmers boatloads of money to write optimized code; because hardware is cheap, they can do fine without that optimization.
Whether we thing the pricing is correct or not is a different question I guess. We could theoretically raise computing prices artificially (say through taxation).
Performance-wise, it's bottom of the barrel quality. A single developer has done, in this compartment (Ripcord), much better than their team did. If we want to consider similar languages, Microsoft did enormously better with VSC.
The result is, sadly, that Slack is still widely used.
For the most part users or companies buy features, not performance. You can have the fastest software ever but if it doesn’t have the features I actually need to use it then, sorry, no dice.
If performance is one of your features then fair enough, but it’s not for a lot of software.
This is NOT true. There is almost always an expectation that your application have a reasonable performance profile. It's often not a captured requirement, but I guarantee you once things slow to a crawl you WILL get an escalation.
Do you know what happens when escalations become too frequent? Progress grinds to a halt as developers have to churn out of what they're doing to address non-functional issues.
Somebody operating at a product owner level rarely has any concept of this stuff. It's up to the engineering team to be the bad guy and tell him or her that they're not going to get their laundry list satisfied in the time that's available and actually deliver usable software.
But “reasonable performance” is relative and context dependent
- If your product does not work it does not work, regardless of what you have in your requirements.
However, there is considerable economic incentive to keep Koomey's law going - mostly from mobile these days, since increased computations per joule means longer battery life.
[1] https://www.goodreads.com/book/show/2276288.A_Discipline_of_...
It looks like most software engineers are still unaware of this. Why do we still encode Unicode strings as UTF-8 strings in memory, why not as double linked of Code points? Not because we cannot, but because it is faster and uses less memory.
Another big problem is related to multi-user modifications of shared data structures and that high-level semantic modelling is hard for most people. If multiple agents (people) are modifying a data structure, then those changes need to be combined in a semantic correct way. A complication factor is the fact that most data structures are distributed (also due to the memory pyramid) and that a lot of effort is needed to synchronize modifications.
Yes, this is wrong in that sense, but by learning C you learn how C works, and therefore how your computing systems, which are going to be C at some level because we live in Unix's world, work.
I think this can trip newbies up. Especially from bootcamps, who don't understand why their code is working, and I feel like replying with "well think about how the object is stored in memory" because none of the stuff that's built on that will make sense without it. I'd like to understand more myself. It would be useful to have some idea of what the compiler is doing with things like lexical scopes. I think that's the only way to really understand them as the abstractions are too leaky.
I dont mean you can then write these languages, but you can look under the hood and read the source code (also if you want to write extensions it is useful).
You mean, of CUDA kernel programming. The basics of CUDA as an ecosystem are not that much about a particular language and there are bindings for many languages.
What I find is that programmers I've worked with who have been the most preoccupied with performance always seems to have focused their optimizations in the least valuable areas. It's as if performance mania is directly correlated with a kind of architecture blindness.
Yes, it does save a few function calls and instantiating a class - but that's not where the real bottleneck is. The proposed changes would require rewriting large amounts of code elsewhere, making the logic more verbose and complex.
It took some effort to convince the person that the trade-off isn't worth it. I tried to be diplomatic, but I'm afraid they took it personally that the pull request was declined.
I wish I could have proved my objection with some metrics - how many milliseconds were shaved off by the proposal, versus the increased "cyclomatic complexity", as a measurement of its detrimental effect on the code base.
Rather than rejecting outright, did you consider blocking the pull request and asking for benchmarks demonstrating that the optimization was worth the complexity increase? Maybe they would learn more from that process than from an explanation of why they are misguided.
The person put effort into the code, and I didn't feel that they were convinced of my argument against it. Their changes were more efficient logically, and I'm sure it would have saved a few milliseconds per call - whereas my claim of increased complexity was a more subjective judgement.
In the end, it came down to trust/authority - but they might have been more convinced, or felt better about the decision, if I could produce some numbers from my side, like a measurement of cyclomatic complexity based on static code analaysis.
Perhaps that could have justified the existing higher abstractions, to weigh their (almost negligible) performance cost against the value of reduced complexity - keeping the code base simpler to understand.
Mid-level developers haven't had a lot of those problems yet, but they have read articles about them, and those articles usually do silly things to try to prove a point in a simplistic situation.
We had a developer that was obsessed with getting rid of jQuery for performance reasons, but repeatedly failed to recognize that the raw JS functions he wanted to use didn't work on IE and/or Safari, IIRC. Beyond that, we also supported Android 4.4's webview, and he didn't even have a way to test that. He ended up leaving the company for other reasons, but it was a constant battle to keep him in check with that stuff.
(It required compiling the Cordova app, and so long as you kept doing things the same way as the rest of the codebase, it almost always just worked. So we didn't require junior/mid-level devs to test it on every change. It did get tested eventually before the app got a new version, though.)
Other industries know human investment doesn't scale, but even still it is absolutely professional and so it must still be accounted for. Software intentionally ignores this and then looks for things to blame when the tech debt becomes overwhelming.
I think that the article misses two points. In one side the more accessible writing code is, the better. To reduce the amount of knowledge and discipline that you need to code is good.
On the other side, discipline is still needed to design APIs, or just to maintain consistency between calculations and data as it moves thru your system, or keep technical debt in check.
> does that mean our tools should not let us write code for which there’s no test?
That depends on how your team/company wants to work. If you are doing Test Driven Development, it seems good that the tool forces you to do the test first. But, TDD is not the only or best way of developing software, so generic tools needs to support different paradigms.
I love code formarters for all this reasons. They relieve developers of the need to follow some basic code style guidelines, you use less self-discipline on writing good style and more on writing good APIs. But, to have a code formatter you need an agreed standard.
I try to make to write code easier for myself and other people, because implementing business requirements is already hard by itself. The more time you have to spend "coding" and the more you can spend thinking, the better.
Consider this line from the article:
> you don’t need TypeScript to write good React Native code, just Javascript and some discipline
The author provides this as an example of the "disciplinarian" approach, which the author says "doesn't scale."
It's an interesting example, because learning a new way to code requires discipline. IME, the main obstacle to TypeScript is that you have to have the discipline to learn TypeScript and endure the compile errors it generates.
I think you could rewrite the author's example the opposite way:
"You don't need to write careful JavaScript, you just need enough discipline to learn and use TypeScript"
But this is a different kind of discipline; it's the discipline to seek out a "better" (or at least more scalable) way of doing things. You typically hear static-typing fanatics calling dynamically typed languages "undisciplined."
TypeScript is an order of magnitude less popular than pure JavaScript, even considering just new code on Github. TypeScript may be better, but it isn't taking over the world--not yet, at least--because it requires (enforces!) more discipline to get started.
* There is no automation for writing good code and for good design - and that too requires discipline.
* In many cases, harnesses which relax discipline requirements have costs: dependencies, larger surface area for compromising security, performance...
* Very often, the mass of undisciplined coders rely on libraries and other core pieces of code written with a lot more discipline.
Military orgs around the world are turning into smaller head counts of disciplined people. That is known since the Battle of Thermopylae but maybe getting lost from time to time. There is also a lot of military tech that is trying to prevent soldiers from shooting themselves in the foot, which fails from time to time.
We also know that just adding more developers to project is not increasing velocity and that small focused teams outperform large teams by orders of magnitude.
Problem we have to tackle are big orgs that need a lot of code and have high dev turnover. That is why frameworks like angular and tools first are so important, they help to scale code better than plain code or plain js.
C++ while obviously useful, while it obviously 'works', and while it's existence in the context of history is also obvious ... is a giant clusterF of an anti-pattern programming language. The 'smaller, better language trying to get out' should have happened long ago, it didn't so maybe that will be Rust, but that's a big big leap and it's going to take many years yet.
what the heck do those phrases mean?
Simply put: all abstractions are leaky, all abstractions have trade-off, and you cannot necessarily rely on some abstractions in every context.
The GC example that the author uses is a good one: having GC in a language does not magically remove all memory allocation issues. Believing this means you just don't have much experience programming (or you've been very lucky). I've been working mostly with GC languages and we have memory leak every now and then, good luck to fix that without discipline. Embedded software frequently relies on (very) limited compilers and libraries, most of the time there are no GC and until recently there were even no malloc/free for $B things shipped to space: only static arrays and discipline my friend!
TCP is another good example: sometimes you need to craft you own congestion management, packet discard rule or what not.
ORMs have made my life easier but I've still had to spend days on complex hundreds lines SQL reports. The list goes on.
It's not "I had to go through this, you should too" out of frustration. It's because at some point the same kind of issue will appear again, even though the current available abstractions may seem to shield us from it.
It might not scale as easy as you wish, yet you need some disciplined seniors to review and teach your juniors. This very article ironically proves it :)
Different situations can call for different tradeoffs.
Because of Moore’s Law, technical advancements, and because of the increasing economic benefits of programming, this tradeoff has shifted to more automation over time.
These trends are still occurring.
As in automation of manufacturing, the tradeoffs are sometimes not as obvious as they appear.
Keep an open mind and consider the future.
Are there examples where this applies to shorter time periods?
e.g. the discussion on the evolution of C++ language is fantastic, but it took place over.... years? A decade?
If I'm this person in my org trying to write better software for my project, does it really help to not be more disciplined? Before the invention of reference counting, wouldn't the developers who did know how the thing really worked be more productive?
I think this is important to call out, and not go to the other extreme. Discipline is definitely not scalable, but neither is ... the lack of it? Inertia is very strong in production software systems.
For the record, I do tell people they should learn C so they have a better idea of how computers work. That emphatically does not mean I want them to start writing actual production code in C, unless there’s an extremely good reason (and that hasn’t happened for me in a decade or two). Advising people to gain a deeper understanding of their tools is not “hazing”.
I've heard not so good stories about 100% code coverage. Metrics do not scale.
Software development is a balancing act of visible results, technical debt, available workforce etc etc. Life (cells and above) is not perfect but it matches environment.
> no strong motivation to become more disciplined
Bug on production bug is much better motivation to write tests than some discipline. Write code so other people want to work with or search another place.
> either torpedo your whole idea or turn it into not doing the thing (see OOP, Agile, Functional Programming)
A lot of languages are not OOP by Alan Kay, Agile and SCRUM subverted. That is marketing, when benefits is in the badge not in the substance. Those who chased idea do well.
Or rather, human systems are flimsy.
"knowing the value of everything and the cost of nothing"
and
"knowing the cost of everything and the value of nothing"
and it is extremely context-sensitive esp with respect to the team of people making the software.
As humans we tend to forget that programmers are ultimately just machines that turn coffee into reads and writes = )
Anything that can be automated can be scaled better. Too many words to communicate simple idea.
Who is arguing this?
> As a proponent of discipline, I see things a bit differently. You’re coming at it from the perspective of “In software, we automated X, and now X is no longer a problem, therefore, we should just keep automating as far as we can”. That’s one pattern.
> I’m seeing from the perspective of: “There’s no way to automate everything, and professionals always need discipline, so let’s start with that, and then see what else can/should be automated”. My pattern is from looking at the history of every other industry in the world.
> Discipline is the rule, not the exception. When cars crash, we test and license people, and issue citations for people who visibly break the rules, and increase driver education. We don’t ban manual transmissions (not enough automation!), or install breathalyzer interlocks in every vehicle. Self-driving cars may solve our remaining problems, someday, but that’s a long-term solution.
> Similarly, it may be possible in software to automate our way out of many classes of bugs (I’m skeptical), but that’s a long-term solution. Discipline is something we can adopt today.
> Two points you didn’t really seem to address:
> First, there’s diminishing returns on technology as preventative measure. Manual refcounting is way better than malloc/free. Automatic refcounting is quite a bit better than manual refcounting. Tracing GC would be a little better than that. Every problem is like this. When you’re down at the “malloc/free” level, advocating for discipline seems silly. (Why not just fix the bad tech first? It’s not hard to improve on.) But once you’re up to about the ARC level, discipline is clearly worthwhile. You don’t need to wait for a perfect tracing GC to advocate for social solutions.
> When there were only 10 cars in the whole state and no roads, licensing and education are probably not high on your list. But once you’ve got an Interstate Highway System, you need licensing, and education, and laws against drunk driving. Nobody in 1965 said “People are dying in car crashes — we’ve got to automate this by developing self-driving cars ASAP!” You look at the limiting factors. At some point, more technology is a huge cost, in time and money, and a social solution would be cheaper and more effective.
> Second, you seem to be using “discipline” to mean only an internal personal strength of will, but that’s not really what the word means. Yes, we know that relying on willpower alone doesn’t scale. The goals are quality and accountability, and in almost every other field, we accomplish this through laws, regulations, certifications, trade groups, professional associations, and so on. Those do work. Check your dictionary: the definition of the word “discipline” includes rules and punishment!
> Your surgeon is not performing a splenectomy while drunk, but that’s not because there’s a breathalyzer installed on the door of every operating room. We didn’t use technology to solve that problem. We used training, regulation, and (severe) penalties.
> That’s what software development needs today. We can’t keep pretending that more automation is going to solve everything. You’re not going to find a new “GC” level advancement every year, because there aren’t dumb inefficiencies like “malloc/free” still sitting around. Once the low-hanging fruit of basic automation have run out, there’s no substitute for professional discipline.
--
Wasted few minutes reading this blog post.
That is important point for arguing with people who say things like 'ORMs are bad I can write my SQL queries', 'Angular is bad I have my perfect approach for vanilla js'. When such person will have to explain their 'perfect' approach to 10 developers and then such person will have to keep doing code reviews so that approach is still used by those 10 developers it will be an eye opener