Rob Pike’s Rules of Programming (1989)
users.ece.utexas.edu
users.ece.utexas.edu
I wish people would follow this rule and just let stuff work. I recently encountered the most extreme version of this I've ever seen in my career: a design review where a guy proposed a Redis caching layer and a complex custom lookup scheme for a <1GB, moderate read volume, super low write volume MySQL database. And of course he wants to put the bulk of the data in JSON fields and manage any schema evolution in our application code.
Can't we just let stuff work? I'm no fan of MySQL, but can't we admit that a ubiquitous and battle-tested piece of technology, applied to a canonical use case, on tiny data under near-ideal circumstances, is probably going to work just fine? At least give it a chance before you spend days designing and documenting a bunch of fancy tricks to save MySQL from being crushed under a few megabytes of data.
I have a problem with this rule because what I see happening is people taking it to heart and no longer thinking about what they're doing performance-wise. And then the program is working 1000x slower than it should, at no extra gain (and often a loss) of readability or safety, just because someone decided to use O(n) data structure where O(1) would do, or keeps repeating the same computation thousand times instead of ensuring it's done once.
So to your "let stuff work", I want to also add: "understand the work you're putting the computer through", and "don't do stupid things".
This rule a la Pike is about doing things like writing assembly or manually unrolling loops in noncritical parts of code. However, in some code, almost everything is on the critical path, and that requires architecture. I’m thinking of Carmack’s single function game loop here.
I remember working on a project that claimed to want 10,000 transactions per second. “Okay”, I said, “how much can be accomplished in 100us?”
They looked at me like I was an idiot. “No, it’s going to be clustered and pipelined.”
“Ok, if you can do that maybe you have a budget of 1-5ms.”
“No, we’re going to give each transaction up to a second to get done”
I smiled and admitted that they were obviously a lot smarter than me. Oddly enough the product never saw the light of day.
Let the "geniuses" figure out that to do 10k transactions/sec with processes that can handle 1 transaction/sec would take.....10k processes. Sure, doable, in specific contexts, given enough resources. But they sure as heck aren't free resources!
Usually when you encounter one of those it's a rewrite/rearchitecture of a whole module/subsystem before you see any gains. Been there done that, not excited to repeat it again.
A retailer app was very slow. The developers proposed a newer faster server with a newer Oracle version.
Then I took a look at one of the slowest queries. Changed it so it could use the indexes in a better way and the query went from +20 to 0.7 seconds.
The developers measured the query was slow, they checked the query used indexes and they were right that a server with more RAM and a newer Oracle version could improve the speed. But they missed that the query had to fetch a lot of data (using indexes) before it could start filtering the data. The only thing I did was to change the query so it could filter the data first.
knowing where to put the bolt - 95 bucks per hour.
If you actually know where (or each one of the multiple wheres if several places collude), that is usually very close to knowing why; often the same.
They should have had the intuition that if a query takes 20 seconds, even in a testing scenario where the system is not bogged down, it must be churning through a lot of data all over the place. Then think: does the query actually need to be looking at a lot of data? Maybe it's wastefuly looking at more records than necessary. They didn't imagine what the machine might have to do to satisfy the query, just accepting it as a black box that the DB has optimized as well as it can be (so just throw hardware at it).
But if it's an in-house thing then there should be open communication lines. The SRE paradigm makes sense there, where I see a SRE as part dev, part ops. They can identify perf issues as a software problem and either fix it or send it back to the authors.
I am not criticizing anyone because I was just lucky to notice the query could be rewritten. But it was also the fact that I had a little more knowledge about how the db engine handles queries.
So we all looked in the right direction, we all found the bottleneck but the solution was different based on different knowledge.
Your experience exactly shows why having a diversity of opinions/backgrounds/expertise on a team is a very valuable trait. Had no one realized you could rewrite the query, would scaling vertically and upgrading Oracle been a fatal mistake for the team? Probably not, but damn if it wouldn't have been a big waste of time/money.
Even with in memory caches I've seen systems grind to a halt by death of a thousand cuts, dictionary based entity attribute systems where each attribute is looked up individually. There seems to be a mentality that constant lookup == free lookup and devs don't seem to realize that constant * $bignumber == $biggerNumber. Caching shouldn't be granular.
Obligatory latency numbers every programmer should know: https://gist.github.com/jboner/2841832
Say you consume a Restful API to hidrate order data in another system. Do you fetch orders as:
/orders?ids=id-1,id-2,id-3
or separate calls to /orders/id-x
which can be cached and retrieved by id as a memoized function?Well if you had to pick only one the second is probably better, but the best would be abstracting away order fetching in application code to always fetch single orders and behind the scenes looking up the cache for singles and pooling all the misses into a single request to the plural endpoint.
> but the best would be abstracting away order fetching in application code to always fetch single orders and behind the scenes looking up the cache for singles and pooling all the misses into a single request to the plural endpoint.
Unless a significant amount of requests have 100% cache hits then I doubt a local cache will make much of a difference at all, all it's saving is a bit of bandwidth.
I'm trying to never implement any caching if I can help it. The database itself does caching already as well.
And if you DO need caching, keep your hands off of the application; add a cache layer in front, or between the application and the database. But don't invent it yourself.
And if you DO need caching, keep your hands off of the application; add a cache layer in front, or between the application and the database. But don't invent it yourself.
so, redis?I’ve worked on some pretty high traffic systems, with pretty large data volumes, and very, very rarely have I actually needed to cache simple DB reads. And it’s hard to properly cache complex ones anyways, because cache invalidation is so hard for complex queries. Likewise, it’s very rare that I’ve needed sharding. SOMETIMES you need these things, but mostly people are adding a lot of extra complexity and cost for no good reason.
Even when reading from disk via mmap, hot pages are in memory.
Sometimes postgresql is faster than redis because all it needs to do is read something from memory and spit it out in the right format.
Looking up a normal sized record by id, with good networking in your data centre, takes ~1-2 ms round trip whether you’re reading from Redis/Memcached or MySQL/Postgres, and either can handle massive read load if sized properly. The cache just ~doubles your costs, is one more thing to patch/maintain, and introduces new sorts of bugs/outages.
I actually don’t think “premature optimization” is all that bad in the general case, but you have to be educated about it (and I’ve met a lot of people in Python shops who think that Pandas or multiprocessing will cure all performance ails), and you should specifically think about how costly is a given optimization going to be to maintain or back out of if you’re wrong about the performance benefits.
In general, I’ve never worked on a Python project where we didn’t have to do weird, inordinately expensive things to work around performance issues (though I’ve never worked on a rudimentary CRUD app either) nor has “just throw Pandas/C/multiprocessing at it” ever adequately addressed our most significant performance bottlenecks (usually the solution looks something like Spark, all to do something that would have been sufficiently performant with naive Java or Go). This might just be my experience working on nontrivial SaaS apps; maybe if you’re just doing straight data science or CRUD apps or workloads that aren’t latency-sensitive (mind you, we struggled to keep per-request performance in the tens of seconds, so I use “latency-sensitive” very loosely), Python/Pandas will be just fine. We also ran into other problems with Python, such as packaging and distribution; notably our lambda functions were routinely too large because the pandas branch of the dependency tree was itself more than half of the permitted artifact size (to work around, we switched to Fargate tasks, which have a much larger size limit but take 30s or minutes to boot up).
Did a performance problem exist?
If not, then I would never promote that person to senior engineer. A working program should only be re-engineered for performance if it wasn't meeting the performance contract agreed upon when the program was written.
Early on I had a senior guy mentoring me on a project involving a tool called PowerBuilder. He chose a design that didn't fit the problem-space well but it fit the "PowerBuilder Best Practices" so he implemented it. The performance was abominable and he should have known it would be: he too was a smart guy. But he had a hard time seeing "big picture" design.
Asking for benchmark just gets a repeat of "our worst moment is MySQL and we can solve that with some NoSQL cache".
I wish there was a quick remedy for such people. But usually they are new, based on what I said. So they should be Junior Developer, not Architect. Sure, you can suggest things, but the answer is nope.
A lot of people, though, may never get much experience supporting their own architectures, because they swoop in, then swoop out, never staying at a job long enough. Or support gets assigned to another group. One thing I like about Agile is that the team that creates the code is the one that supports it.
We went with redis for the "PoC". I'd tried explaining that if a query is only made once every 24 hours and the data can't be cached longer than that because it's considered too out of date then a cache is pointless extra complexity.
He wasn't having it though, so he made us build it and demo it to a room full of people. Fortunately the room understood the simple explanation and he listened to them where he wouldn't listen to the dev team, so a few weeks of work was scrapped there and then.
The problem is that it's boring, and there's a lot of developers that create work and complexity to make their own jobs interesting.
Example: Google Chrome codebase was allocating lot of std::string and also someone used a Set to check membership of single item. [1]
I mean, if you say like this, many people don't even care about algorithm complexity.
Doesn't help that people want to write Python in the monster that is C++.
https://groups.google.com/a/chromium.org/forum/m/#!msg/chrom...
From the post you linked though:
> Not reserving space in a vector when the size is known
std::vector::reserve() is actually not something you should always use when you are adding a number of elements as it will (typically) grow the vector to exactly what you ask. If your function then gets called in a loop to append to the same vector multiple times you end up with quadradtic run time that is normally avoided by the geometric growth done when you just append without reserving.
Depending on where it was done (fast path or not), this could be just fine.
if (std::set(itr.begin(), itr.end()).count(element)) { _____ }
is it tempting for someone than something likestd::find(itr.begin(), itr.end(), element) != itr.end())
?? I don't know. That said, C++ STL quite undiscoverable.
That's the point of the first advice though...
Inexperienced engineers will nitpick about what is often minor “performance optimization”, clearly not seeing the bigger picture. Example, why should we spend precious developer time to rewrite some code using something that is often less readable when it’s called once every 30 seconds?
To the folks who do this: you are better off spending the time making data-driven decisions and optimizing for the big picture. In other words measure first than come up with an optimization that has a large impact on the system. Not this micro-optimization, I-love-to-tickle-myself stuff.
Learn to see the bigger picture.
Bane of my life. A few gigs ago, they had a global Redis cache, a Redis cache per server, an in-app cache, and then MySQL. Needless to say, there were many, MANY bugs that came down to cache coherency and race conditions between them.
[edit: it was a global Redis, not clustered.]
I call that "separation of error-domains", meaning trying to find interfaces where you can separate off parts of your application, so debugging gets easier:
Is the bug in the database, or in our code?
My most recent team had our hottest dataset in dynamodb. Because cloud. So much effort to get our web service's P99 <50ms.
The whole dataset fit easily into RAM.
Thru (way too much) effort, I was able to introduce Redis. First shared. Then eventually one mini-instance per EC2, running alongside nginx & nodejs.
It looks like an excellent candidate to put it entirely in RAM and trigger sync on writes only, why on Earth would you need anything else.
You don't wanna know how that was done before. It is like < 1 GB of JSON as well.
I first read that on Guy Steele's site: http://www.dreamsongs.com/ObjectsHaveNotFailedNarr.html
A couple of years ago I spent quite some time trying to evaluate the tech stack (and general engineering culture) of merger/acquisition targets of my employer. It was quite a fun exercise, all said and done. I encountered all sorts; from a small team start up who had their tech sorted out more or less to a largish organisation who relied on IBM's ESB which exactly one person in their team knew how it worked!!
I discovered this exact method during the third tech evaluation exercise. When the team began explaining various modules top-down and user-flows etc., I politely interrupted them and asked for DB schema. It was just on a whim because I was bored of typical one way session interrupted by me asking minor questions. Once I had a hang of their schema rest of the session was literally me telling them what their control and user flows were and them validating it.
Since then it's become my magic wand to understand a new company or team. Just go directly to the schema and work backwards.
Conversely, I've begun paying more attention to data modelling. Because once a data model is fixed it's very hard to change and once enough data accumulates the inertia just increases and instead if changing the data model (for the fear of data migration etc.,) the tendency is to beat the use cases to fit the data model. It's not your usual fail-fast-and-iterate thing.
> Bad programmers worry about the code. Good programmers worry about data structures and their relationships
And yet, I see a whole swath of the industry hyper-focused on various linters/styling/rules.
It seems to me that what you're actually seeing is an entire industry trying to eliminate all code-related issues, specially bike-shedding ones.
This is patently obvious to anyone who was forced to waste their time in code review iterations discussing, say, where a brace should go and how many spaces someone should have added.
(If multiple people are arguing back and forth in code review -- when following the WIR rule -- tell them about the WIR rule and that should settle it. If not, you have bigger problems in your team.)
Why though? I am not going to go with suggestions if they make the code less readable for me!
I personally get super annoyed when people keep pointing out style issues, but our CI tool can notify me of issues with my commit until the end of time without me getting frustrated with it.
It also remove all debate in PRs about style and formatting.
(note: before prettier, I was fairly particular about how I formatted my code, and I disagreed with prettier in some cases, but now, I love having one less thing to think about)
> And yet, I see a whole swath of the industry hyper-focused on various linters/styling/rules.
I've come to a severe distaste for this good programmer/bad programmer mentality I've seen on the internet for, I guess decades now
There is skill in programming, yes, obviously. But this simplistic divide seems to me to be more about putting one's own ego on the superior side. It leads to simplistic heuristics and flames rather than nuanced discussion
In this case, in my opinion, linters/styling/rules help people to focus on what matters. And sure, with sufficient skill you might not need any of that to help you focus on what matters. But so what? It's better if we can make the trade more accessible, and can make it so people can focus on what matters with less experience
It isn't Guy Steele's website. That page was written by him but the website is owned by Richard P Gabriel.
That carried me very well.
ML researchers drown their algorithms in huge tables of results, effectively spending time on "how well" rather than the "what".
It often leads to things being added as long as they are better, with the conclusion of it being a gargantuan monster of models and hand-engineered changes. All with no one understanding how the whole things works as a single unit.
Flow charts are incredibly effective as the top most layer of abstraction. Does the whole process, when viewed in an end-2-end manner, make sense ? We dive into the details only if it passes that sniff test of a flow chart.
I might be missing the point being made here, but they can claw flowcharts from my cold dead hands.
But perhaps you meant "flow-of-data between structures" -- in which case we have agreement on engineering, but a muddle on semantics.
> In 1976, still back in the USSR, I got a very serious case of food poisoning from eating raw fish. While in the hospital, in the state of delirium, I suddenly realized that the ability to add numbers in parallel depends on the fact that addition is associative. (So, putting it simply, STL is the result of a bacterial infection.) In other words, I realized that a parallel reduction algorithm is associated with a semigroup structure type. That is the fundamental point: algorithms are defined on algebraic structures.
This is also exemplified in the analytics infrastructure used at stripe: https://www.infoq.com/presentations/abstract-algebra-analyti...
* lhs is before rhs
* There is no data between lhs and rhs
At the moment of computation, you can build a new structure that commutes by enumerating the data. I guess it's true that you need a commuting intermediate data structure to be able to distribute.
I guess the key is to know how to deal with things that are only mostly true.
That exactly proves his point. Systems that are associative can be processed by the parallel algorithm he was thinking of. Floating point numbers, if you care about their non-associativity, cannot be processed by that algorithm. So the validity is that algorithm depends on whether the system is associative.
In any case, the point Stepanov was making is that if you want to be able to use a certain algorithm, then you have to make a choice to represent your data in a way that enables that algorithm, and the way you know whether the structure is appropriate for that algorithm is the algebraic properties of the structure.
They are destined for failure.
Actually that was Donald Knuth - it's an urban legend that it's an urban legend that it was originally Knuth. Hoare was quoting Knuth, but Knuth forgot he said it, and re-mis-attributed the quote to Hoare.
"We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. Yet we should not pass up our opportunities in that critical 3%."
Premature complex optimization is a bad idea, but simple (read, cheap to code) optimization for common bottleneck patterns is a perfectly reasonable thing to do.
That "premature" and "optimization" are undefined and left up for debate is what makes it trite.
The point of most people referring to this quote is never try to optimize anything as you write it. First build your system(?), then measure it, then optimize it. Knuth's point is that this attitude is ok most of the time, but sometimes it's not ok. Another way of putting this is that most of the code, for most applications, isn't performance critical. But some code is.
Sure, you can't always tell in advance, but sometimes you can. This is sort of the difference between terrible software that will always suck and well crafted software, no amount of measurement or after-the-fact optimization will turn that terrible software into well crafted software.
The other aspect that I think is often missed is that these observations are often made at different scales. You can look at relatively short algorithm (let's say merge sort) and it may not be obvious which instructions are the ones that need to be optimized and what the bottlenecks will be, execution units, data access e.g. So you start with a reasonable but maybe naive implementation and then you optimize from there. That's a pretty solid idea. But taking that idea to a higher scale level, e.g. saying we're going to build this huge system with a billion lines of code and so we'll just throw something together and measure it isn't exactly the same thing, that's a pretty problematic idea. You need to be able to anticipate what the bottlenecks in your billion line system are going to be because finding that out after you've written a billion lines could be a big deal.
[EDIT: and really this whole long story is why these sort of rules don't work. Because the people who know (have the experience/craftsmanship) don't need the rule and the people who don't know won't understand it. It's like reading a book about sword fighting and then trying to go into a sword fight... The reading can complement your training but can't be substitute...]
The scientific method has a similar problem. A scientist should form their hypothesis before gathering data to evaluate the hypothesis. If a scientist fails to do this, and starts engaging in p-hacking or data dredging, the quality of their research greatly declines. But proving that a hypothesis was obtained before data was collected is not usually provable when just looking at the publication itself. And further, there are ways that data dredging can unintentionally sneak into the scientific process, especially around the phase before hypothesis- observation.
This kind of idea has large technical impact, but doesn't have a solid technical reason. It's proof is closer to aesthetics than reason. And much like other aesthetic beliefs, a population believes it based on no deeper reasoning. Only exclusion or indoctrination can ensure the population's view, and only illogical rhetoric will change it.
What to do about this? Two things:
STORE YOUR TIMESTAMPS IN UTC. NOT US Pacific or any other local timezone. If you start out with the wrong timezone you'll never be able to fix it. And generations of programmers will curse your name.
Keep your data structures simple enough to adapt to the future. Written another way: respect the programmers who have to use your data when you're not around to explain it.
And, a rule that's like the third law of thermodynamics. You can never know when you're designing data how long it will last. Written another way: your kludges will come back to bite you in the xxx.
For example, if you wanted to track the history of when the shop actually opened, it would make sense to store a UTC timestamp.
Correct, but that makes this a rule with much more limited applications than many people are going to interpret it as.
It's important to the advice to make explicit that the use of “timestamp” in that sense is intended, because “timestamp” is also in many contexts “the data type that combines date and time of day and, perhaps optionally, time zone information”. The application of “timestamps” in the latter sense is not limited to when they represent “timestamps” in the former sense.
It was when I showed them that we also have a 'shot at' location then proceeded to show Christmas eve photos showing the UTC time converted to the viewers local timezone (not always evening, not always Dec 24) alongside where the photo was taken. Just as in space-time a photo needs both a time and a place.
Still, for things that have already happended, storing them as a UTC timestmp is almost always the correct thing to do.
What difference does it make if the timestamp includes the timezone? The UTC value can be recovered. In some applications the timezone is useful e.g. when intraday times matter.
If you record the timezone you can convert. Even then, it is easier to use UTC just because everyone else does and so you can feed UTC into any third party library and it will work.
You can always translate UTC to a local time in a given timezone. With IANA zoneinfo, you can do that correctly even for historical data in places where timezone rules changed in the past.
You can always calculate elapsed times correctly by taking differences between UTC timestamps. With local times you can't. Because daylight time transitions.
If you started with a local service, UTC lets you expand globally without explaining to your new customers why your timestamps are not in their timezones.
Daylight time transition days. Because daylight time.
Oddball daylight transition rules. Because Indiana USA, from the legislature that almost wrote a law declaring the value of π to be 22/7.
Because almost everybody will understand your decision, even after you're gone.
Also in support of Rule 5, see Eric Raymond's treatment of Data-Driven Programming:
"Even the simplest procedural logic is hard for humans to verify, but quite complex data structures are fairly easy to model and reason about. To see this, compare the expressiveness and explanatory power of a diagram of (say) a fifty-node pointer tree with a flowchart of a fifty-line program. Or, compare an array initializer expressing a conversion table with an equivalent switch statement. The difference in transparency and clarity is dramatic. See Rob Pike's Rule 5.
"Data is more tractable than program logic. It follows that where you see a choice between complexity in data structures and complexity in code, choose the former. More: in evolving a design, you should actively seek ways to shift complexity from code to data."
http://www.catb.org/~esr/writings/taoup/html/ch01s06.html#id...
http://www.catb.org/~esr/writings/taoup/html/generationchapt...
If you make an optimization that was not at a bottleneck, you did not make an optimization.
It doesn't matter how optimized your computations are if you're spending the whole time waiting in IO. And don't forget that the program is generally just a piece of a larger process.
Anyone want to share their main takeaways from these books?
This can become a tragedy of the commons in desktop and mobile apps, where you don't know how much memory the end user has or needs, but you do know you aren't paying for it.
This is absolutely not true. Just because you have enough memory does not mean that wasted memory couldn't be better used - e.g. for disk cache or to run more tasks.
In other words, all engineering is time- and cost-constrained. Anybody can build a good chair for $10,000 or a good PC for $100,000. Doesn't mean it's good engineering.
And some people can build a great PC for $1,000 that runs circles around the good PC for $100,000.
There's so much more to engineering than thinking in terms of time and cost constraints. Those are real constraints, but they're not the most important.
Engineering is design. If you have good design, good insight, you can do things that people with infinite time and budget could never dream to achieve. You can start making a product that's a hundred times more powerful for a tenth of the price in a fraction of the time. If you don't have good design, good insight, then no amount of time or budget can help you.
BE LOGICAL! Of course you first fix the big bottlenecks.
>good PC for $100,000. Doesn't mean it's good engineering.
Of course it is...or can you gold platter a pc case?
1. Adding the optimization didn’t make the code more complicated.
2. Adding the optimization didn’t introduce a bug.
3. This part of the code will be a bottleneck in the future. The time spent optimizing is a write-off if the project is canceled or that portion is replaced.
I agree with the general thrust of this. But it's worth pointing out that often the easiest way to prove where a bottleneck is (or at least) isn't, is to try an optimization and see if it helps. I like profiling tools immensely, and this kind of trial an error optimization doesn't scale well to widespread performance problems. But there's something to be said for doing a couple of quick optimizations as tracer bullets to see if you get lucky and find the problem before bringing in the big guns.
The last three rules bug me. I wish we had a name for aphorisms that perfectly encapsulate an idea once you already have the wisdom to understand it, but that don't actually teach anything. They may help you remember a concept—a sort of aphoristic mnemonic—but don't illuminate it. The problem with these is that espousing them is more a way of bragging ("look how smart I am for understanding this!") than really helping others.
For example:
> Rule 5. Data dominates. If you've chosen the right data structures and organized things well, the algorithms will almost always be self-evident. Data structures, not algorithms, are central to programming.
OK, well what are the "right" data structures? The answer is "the ones that let you perform the operations you need to perform easily or efficiently". So you still need to know what the code is doing too. And the algorithms are only "self-evident" because you chose data structures expressely to give you the luxury of using simple algorithms.
Is "data structures" the correct term here? Assuming I'm not misinterpreting, the usage of "data structures" can be misleading - one usually thinks of things like BST's and hash tables, which are inherently tied to algorithms. I feel like "data modeling" better captures the intended meaning here.
If you're a celebrity computer scientist and a problem has been 'squashed' to the point where there aren't any more 80-99% "hot spots" I guess you can just parachute out of there and on to a more interesting problem.
However, if you're paid to work on a particular thing, sooner or later, you will fire up a profile and say "oh, great, I don't have a hot spot anymore, just a bunch of 10-30% 'warm spots'". At that point you need to attack problems that aren't traditional bottlenecks if you still want to get speedups.
Moreover, the things that you learn from repeatedly attacking those 10-30% 'warm spots' might be fundamentally different from the learning you get from, I dunno, taking some O(N^2) monstrosity out of the "99% of the profile" and Declaring Victory.
Rule 3 and 4 are gold and always true.
Rule 5 is the key to good design.
That makes sense, to me.
One of the first things that we learned, when optimizing our code, was to use a profiler.
Bottlenecks could be in very strange places, like breaking L2 caches. That would happen when data was just a bit too big, or a method was called; forcing a stack frame update.
We wouldn't see this kind of thing until we looked at a profiler; sometimes, a rather advanced one, provided by Intel.
Eric Evans' Domain Driven Design is a good book on the topic.
A key point, though, is that you learn the right domain models/abstractions over time. Refactoring is critical as you gain more insight into the domain. If you’re constantly questioning your modelling of the domain, and refactoring towards a better one, you’ll end up with a great model and thus a clean, understandable, easy to extend/modify system. If you stick with whatever abstractions you chose at the start, when you knew way less about the domain/business problems, you’ll likely end up with poor abstractions, and a code based that’s slow, tedious and error prone to modify.
Convincing the business that it’s worth setting aside time to constantly refactor towards better domain models is often the hardest part, but crucial.
The problem is not the self-evident algorithm, but the delicate implementation (or god forbid, at scale).
Take in 1000 web requests per second. The data is all strictly validated and has about 60 fields a record/req plus dealing with errors.
How does that go from webserver to (rolls dice) kafka to a (rolls dice) cassandra that can be queried accurately and timely? How much does that cost?
Oh, that's not a programmer problem. Except it is. Creating a fantasy niche of describing problems as data vs algorithm is the canonical ivory tower disconnect.
I'm making an argument that the stark reality of what's hard in software development is not the simplistic "Rules of programming", which have limited utility.
The reason the "rules" aren't self evident (or followed), is because we live in the reality of disparate functionality paired with an ever-changing technical landscape. You can't just make a DB KISS abstraction and expect it to hold with all the different repository types like (rolls dice) Athena after using (rolls dice) CockroachDB. There are concerns that are not purely algorithmic vs data structure that are far more influential and important to understand. Even knowing these details and cases, becomes less useful as time goes on and new technologies emerge.
> I am having a hard time understanding what you have written
If you're not interacting with new environments, tooling, and problems, regularly (every year or 2) you don't encounter the real pain which is far more important to your career and your ability to produce functional software. Reading almost every postmortem, the number of lines attributed to "we changed the data structure to O" is dwarfed by "we learned that technology X does Y, so we had to do Z".
This is only incidentally related to distributed systems, which is indicative of a disconnect with the problem described. Of course when you sit around in the same environment for a long time, you can observe and optimize on structure and algorithm, but that's not getting you to market (you're already there) and that's the nature of maintenance...not just being a fire extinguisher who is on call.
It is the responsibility of tech leaders to minimize this (accurate) stereotype. Choose boring technology, and only build your own or choose something exotic when it gives you competitive advantage - because the reality I also see is that 99% of devs aren't working on anything new or unseen in the field. Even at the FAMANG companies, most people I know are working on boring problems.
So when your CTO or architect or whomever buys into the hype for X technology, make a good argument against it by proposing a better solution.
Instead, day 1 is installing python or java, running hello world and talking about pointers, binary, encoding, logic gates, etc.
We should be teaching students on day 1 that code is a liability and to be avoided whenever it is convenient to do so.
"1. From Problem Analysis to Data Definitions
Identify the information that must be represented and how it is represented in the chosen programming language.
Formulate data definitions and illustrate them with examples."Here's the problem: If you profile software that is 100x slower than it needs to be on every level, there are no obvious bottlenecks. Your whole program is just slow across the board, because you used tons of allocations, abstractions and indirections at every step of the way.
Rob Pike probably has never written a program where performance really mattered, because if he did, he would've found that you need to think about performance right from the beginning and all the way during development, because making bad decisions early on can force you to rewrite almost everything.
For instance, if you start writing a Go program with the mindset that you can just heap-allocate all the time, the garbage collector will eventually come back to bite you and the "bottleneck" will be your entire codebase.
Oh this hurts. I work with a system in Perl that is just kinda slow. Too slow to be good but not slow enough to be useless. Slow enough that if it crashes we have trouble getting things reprocessed in a reasonable time, there's no fat built in to our timelines.
Anyway I've profiled it many times and found exactly what you said. Layers and layers of OO soup, functions calling functions calling functions. There are no obvious improvements. It's overhead, not code.
Rob Pike has written window system software which ran in what now would be called a "thin client" over a 9600 baud modem and rendered graphics using a 2MHz CPU. He probably knows a thing or two about performance tuning.
Also, having programmed more constrained systems decades ago doesn't magically make you knowledgeable on performance on modern hardware with completely different capabilities. In fact, it's probably what causes you to develop a "computers are so fast now, no need to think about performance"-mindset, because everything you want to do could be done in an arbitrarily inefficient way on modern hardware. Performance doesn't matter to you anymore.
It was a fully graphical 800x1024 (or 1024x1024) system running on 1982 processors.
https://en.wikipedia.org/wiki/Blit_(computer_terminal)
> having programmed more constrained systems decades ago doesn't magically make you knowledgeable on performance
Perhaps not but it does mean you've "written a program where performance really mattered" which I believe was the original claim?
I've looked into in that. Blit was monochrome, had an 8Mhz processor, and a relatively large 256KB framebuffer which could but directly written to. There were only a handful commands, mostly concerned with copying (blitting) bitmaps around.
Rob Pike only wrote the first version of the graphics routines - the slowest version, in C(!). It was rewritten another four times over, by Locanthi and finally Reiser.
I don't think any credit should go to Pike for implementing the performance-critical parts of that system.
https://9p.io/cm/cs/doc/87/archtr.ps.gz
> Perhaps not but it does mean you've "written a program where performance really mattered" which I believe was the original claim?
No, it doesn't mean that. You can be wasteful on constrained hardware as well, performance doesn't necessarily matter even on the simplest chips, if what you want to do doesn't need the full capabilities of the system.
However, I am specifically replying to the claim that "Rob Pike probably knows a thing or two about performance". As you can see, Rob Pike handed off performance-critical work to someone else. He probably didn't know how to write optimal code for that particular platform, but even if he did, most of that knowledge wouldn't transfer over to modern systems.
At the very least, he didn't care about optimizing that stuff, or he wouldn't have handed it off. He would've enjoyed optimizing that stuff. And that's all completely fine, not every programmer needs to care about performance. I just refuse to take advice from these people about performance or "premature optimization", because it is uninformed.
This is the complete opposite of Rob’s mindset, which you’d know if you had any familiarity with his work.
I suggest you look up Rob Pike and reconsider some of your hypotheticals about what he knows about. (https://en.wikipedia.org/wiki/Rob_Pike)
That one hits me in the feels because I think a lot of folks focus on algorithms (including myself), and code patterns, before their data and as a result a lot of things end up being harder than they need to be. I've always liked this quote from Torvalds on the subject speaking on git's design (first line is for some context):
> … git actually has a simple design, with stable and reasonably well-documented data structures.
then continues:
> In fact, I'm a huge proponent of designing your code around the data, rather than the other way around, and I think it's one of the reasons git has been fairly successful […] I will, in fact, claim that the difference between a bad programmer and a good one is whether he considers his code or his data structures more important. Bad programmers worry about the code. Good programmers worry about data structures and their relationships.
When I have good data structures most things just sort of fall into place. I honestly can't think of a time where I've figuratively (or literally) said "my data structure really whips the llamas ass" and then immediately said "it's going to be horrible to use." On the contrary, I have written code that is both so beautiful and esoteric, its pedantry would be lauded for the ages-- had only I glanced over at my data model during my madness. No, instead, I awaken to find I spent my time quite aptly digging a marvelous hole, filling said hole with shit, and then hopping in hoping to not get shitty.
One thing that really has helped me make better data structures and models is taking advanced courses on things like multivariate linear regression analysis specifically going over identifying things like multicolinearity and heteroskedasticity. Statistical tools are incredibly powerful in this field, even if you aren't doing statistical analysis everyday. Making good data models isn't necessarily easy, nor obvious, and I've watched a lot of experienced folks make silly mistakes simply because they didn't want something asinine like two fields instead of one.
I.e. too much focus has been put on data structures and not enough on the rest of the tool.
A less efficient data structure, but more focus on UX could have saved millions of man hours by this point.
I think git is more of a power-tool than people sometimes want it to be. It's more like vi than it is like MS Word, but it's ubiquity makes people wish it had an MS-word mode.
So, I think that it's hard to fault git's developers for where it is today. It's a faithful implementation of it's mission.
FWIW, I have never used a tool with better documentation than git in 2020 (it hasn't always had good --help documentation, but it absolutely does today).
> The counter argument would be that git is the poster-child of poor UX, which could be blamed on the fact that it exposes too much of its internal data structure and general inner-workings to the user.
I agree with you that the UI is inconsistent, however I don't agree that it's the result of git exposing too much of the internal data structure.
Data models are the "bones" of an application, as part of the application as code is. Data models fundamentally limit the application's growth, but if they're well-placed, they can allow you to do things that are really powerful.
You always want to have good bones. But the Anna Karenina Principle is a thing [0].
So, applying this, I think baby ideas should not have many constraints on the bones, to allow them to move around in the future. Instead, there should be a ton of crap code implementing the idea's constraints, because they change every week, month, quarter, and the implementer is still learning the domain.
Once the implementer reaches a certain point of maturity in the domain, all of the lessons learned writing that crap code can be compressed into a very clever data model that minimizes the amount of "code" necessary, and simultaneously makes the project more maintainable, interface-stable, and extensible: in other words, making it an excellent platform to build on. The crap code can be thrown out, because it was designed to halfway-ensure invariants that the database can now take care of.
I think most software we consider "good" these days followed this development cycle. multics -> unix, <kversion_control> -> git, ed -> vi -> vim.
I couldn't agree more.
I think the current state of UI programming is like the pathological case to be honest. Too often folks are concerned with representing their database 1-to-1 in their UI instead of representing their view.
If anyone is suffering from brittle UI code, where somehow caching issues and stale data are affecting your application, this is very likely why. You have muddled your persistence and view concerns together and it's not manageable or pretty. What this means for folks using something like React- don't directly use your persistence models in your views, create "view models" which directly represent whatever the hell it is you're trying to display. Bind your data in your view models, and not your views, and then pass the view model in as props.
That's a good one. It's amazing how much complexity can be created by using the wrong abstractions.
It's not an exaggeration to say that such programs are basically big data structures, full of compromises to accomodate the algorithms you need to run on them.
For example LLVM IR is just a big data structure. Lattner has been saying for awhile that a major design mistake in Clang is not to have its own IR (in the talks on the new MLIR project).
SSA is data structure with some invariants that make a bunch of algorithms easier to write (and I think it improves their computational complexity over naive algorithms in several cases)
----
In Oil I used a DSL to describe an elaborate data structure that describes all of shell:
What is Zephyr ASDL? http://www.oilshell.org/blog/2016/12/11.html
https://www.oilshell.org/release/0.8.pre9/source-code.wwz/fr...
I added some nice properties that algebraic data types in some language don't have, e.g. variants are "first class" unlike in Rust.
Related: I noticed recently that Rust IDE support has a related DSL for its data structure representation: https://internals.rust-lang.org/t/announcement-simple-produc...
Totally. I'm building a relational language and start to get very obvious why RDBMS not fit certain purity ideals of the relational model (like all relations are sets, not bags).
I'm stuck in deciding which structures provide by default. Dancing between flat vectors or ndarrays or split between flat vectors (columns), and HashMaps/BTree with n-values (this is my intuition now).
--- > I added some nice properties that algebraic data types in some language don't have, e.g. variants are "first class" unlike in Rust.
This sound cool, where I can learn about this?
https://news.ycombinator.com/item?id=13293290
---
About first class variants:
https://lobste.rs/s/77nu3d/oil_s_parser_is_160x_200x_faster_...
https://github.com/rust-lang/rfcs/pull/2593
Another way I think of this is "types vs. tags": https://oilshell.zulipchat.com/#narrow/stream/208950-zephyr-... (Zulip, requires login)
Basically variant can types stand alone, and have a unique tag. Tags are discriminated at RUNTIME with "pattern matching".
But a variant can belong to multiple sum types, and that's checked statically. This is modeled with multiple inheritance in OOP, but there's no implementation inheritance. Related: https://pling.jondgoodwin.com/post/when-sum-types-inherit/
So basically in the ASDL and C++ and Python type system I can model:
- a Token type is a leaf in an arithmetic expression
- a Token type is a leaf in an word expression
But it's not a leaf in say what goes in a[i], or dozens of other sum types. Shell is a big composition of sublanguages, so this is very useful and natural. Another construct that appears in multiple places is ${x}.
So having these invariants modeled by the type system is very useful, and actually C++ and MyPy are surprisingly more expressive than Rust! (due to multiple inheritance)
Search for %Token here, the syntax I made up for including a first class variant into a sum type:
https://www.oilshell.org/release/0.8.pre9/source-code.wwz/fr...
There is a name for the type, and a name for the tag (and multiple names for the same integer tag). Tags (dynamic) and types (static) are decoupled.
Well, I suspect it's not about Studio per se but rather the git integration but still. Someone avoided a "fancy algorithm" and wasted both my time and the product reputation with a "small n" workaround. Because, git is about small commits right?
I'd like to restate the first rule as "You can't tell where a program is going to spend its life".
— Linus Torvalds
2017 https://news.ycombinator.com/item?id=15265356,
https://news.ycombinator.com/item?id=15776124
2014 https://news.ycombinator.com/item?id=7994102
Pete_D gets credit for the date: https://news.ycombinator.com/item?id=15266498. These rules come from "Notes on Programming in C" (http://www.lysator.liu.se/c/pikestyle.html), which has its own sequence of threads:
2017 https://news.ycombinator.com/item?id=15399028,
https://news.ycombinator.com/item?id=13852734
2014 https://news.ycombinator.com/item?id=7728084
(6. There is no Rule 6.)
And points to the best source he finds for it on the web: http://doc.cat-v.org/bell_labs/pikestyle
Writing stupid code is actually really difficult.
For me, it takes a little bit of iterating before I know just the right place to insert stupid.
(Often attributed to Mark Twain, but similar sentiments were expressed by many before him.)
Kinda goes: 1) Make a bad solution exploring the problem 2) Explore a good idea for how to solve the now-understood problem 3) Mature the good idea through usage.
It's really an issue of not being sure at first what needs to be flexible & data-driven vs handled in code. If make everything data driven, then it becomes this horrible mess where your input is basically a program and your actual code ends up being a terrible interpreter.
I tend to just build things bottom-up, and start with a small bit of functionality, then when I have enough small bits, I bolt them together and decide what I need to abstract at that point, do refactoring on the smaller bits and provide data to them from the caller. Then repeat that continuously until I have all of the functionality I need.
It might be different for other people, but I need to have working code before I can it abstract.
To provide a more concrete example, say I have a function that performs a transformation on a piece data. From what I currently know about the data, I can parse it using a regex. So I code up the function that accepts data, it runs the regex and provides the transformed result. Great.
Now as I'm continuing my work, I notice that some other data requires a similar, but not exactly the same transformation. It can be done with a slightly different regex. So rather than duplicating functionality, I modify the transform function above to take a regex as input along with the data. Everything works as expected.
I get further along in the project and I realize another piece of data needs a somewhat similar transformation, but this time it's just slightly too complicated for a stand-alone regex, it needs to be a function.
The "smart code" way to handle this would be to create another transformation function and call that instead. The "dumb code" way of handling this is to generalize the transformation function such that I can pass in some descriptor for the transformation, and have the transform function return the correct result.
That's the crux of the issue. I rarely have enough information at the time of writing to know just how generalized to make a function. If I created this hyper-generalized transformation at the beginning, but never needed anything beyond the original simple regex, I would have wasted a bunch of time creating code that's needlessly confusing.
TDD is perfectly applicable for development, and would help tremendously with the refactoring aspect, but what it doesn't help with is information that you don't yet know about.
The difference is that you only need to support a tiny fraction of possible features / use cases, but your algorithms need to be correct for a wide range of inputs.
For an algo, let's say it operates on a list, I'll start with test f([]) == 0, and implement f to output the constant 0.
And then go from there.
Tests are good, but they need to be universal for any implementation, which means you often cannot tests the internal details that prove you didn't use bogo sort (picking a pathological example to make the point)
A part of me wonders if Mike was influenced by Rob Pike's rules directly or indirectly. It's also something that an experienced programmer can discover independently easily enough. Mike Acton was clearly heavily influenced by some of the failures of classic OOP design (lie 2 of Three Big lies).
It becomes much easier to understand what data exists and build tools on top of it's schema. But the extra cludge it adds to optimise the specific case of returning less data in a field is often counter productive.
Could you elaborate?
What tends to happen in real life is that the profiling and optimization of bottlenecks is forgotten once the software is "ready".
I would propose Rule 0: Developers are irrelevant, only end users of your software matter.
CPU's have a lot of logic for making in order data access very fast.
The Data Dominates principle is the key, everything else just follow, including n are usually small and preference for simple algorithms.
Nowadays we have exactly the opposite, which is understandable since the field has been invaded by uneducated amateur posers.
True, but emphasis on the speed hack. It shouldn't stop you from thinking about performance in your design. If it means doing less, do less now. If it means adding a speed hack (usually some sort of cache), don't do it until you are sure that you need it.
Rule 2. Measure. Don't tune for speed until you've measured, and even then don't unless one part of the code overwhelms the rest.
I agree completely. But you have to realize that measuring is as much of an art form as optimization is. Especially on today's ridiculously complex systems, the bottleneck may not be obvious. For example a function may take a long time to run, but the real cause may be another function flushing the cache.
Rule 3. Fancy algorithms are slow when n is small, and n is usually small. Fancy algorithms have big constants. Until you know that n is frequently going to be big, don't get fancy. (Even if n does get big, use Rule 2 first.)
Disagree, unless you can prove that n will stay small. You don't know how your users will abuse your program. For example, you may have designed your program to handle shopping lists of a few dozens of items, and then someone decides to import the entire McMaster-Carr catalogue, which has more than half a million items. It may be a great use case you didn't thought of, and that fancy algorithm permits it by scaling well. There are also vulnerabilities that exploit high algorithmic complexity and worst cases. Don't overdo it, but N^2 is rarely a good idea if you can avoid it.
Rule 4. Fancy algorithms are buggier than simple ones, and they're much harder to implement. Use simple algorithms as well as simple data structures.
True, but with the caveats of Rule 3
Rule 5. Data dominates. If you've chosen the right data structures and organized things well, the algorithms will almost always be self-evident. Data structures, not algorithms, are central to programming.
Agree, for me there is a hierarchy in code. From most important to least important: data (structures), code (algorithms), comments.
The order comes from the fact that if you change your data, you need to change your code too, and if you change your code, you also need to change your comments. Going the other way, you can freely change comments, and changing your code will not require you to change your data. Data is the cornerstone.
What does this have to say about the careers and roles of data scientists vs programmers? A data scientists entire job is to categorize and model data in a useful way. In the future, will they fundamentally more important than coders, or will the two roles just merge?
Don’t write complex optimizations until you know you need them, but I think defaulting to code with good O(N) complexity, where it’s simple to do so, is a good default.
The philosophy is really to not waste time implementing optimizations that may not be necessary. Naturally you should reach for the best tool you have in your tool box. So if you language of choice has a hashmap that can be used with no additional work, go for it. But don't wait two days rolling your own red-black tree because it might be better.
Are you sure that std::unordered_map is faster than std::vector? Did you measure?
Every time you access an element in std::vector, you also access nearby ones (thanks to L1 cache, as well as CPU-prefetching of in-line data).
In contrast, your std::unordered_map or hash-table has almost no benefits to L1 cache. (It should be noted that linear-probing, despite being a O(N^2) version of hash-tables worst-case, is actually one of the better performers due to L1 cache + prefetching)
Using maps or sets nowadays is mostly for clarity, as they are used to solve certain kind of problems.
If you need a set, use a set. But don't assume that its faster than a std::vector.
Even then, std::vector has set-like operations through binary_search or std::make_heap in C++, so it really isn't that hard using a sorted (or make_heap'd) std::vector in practice.
--------
Even if you don't plan on doing optimization work, its important to have a proper understanding of a modern CPU. The effects of L1 and prefetching are non-trivial, and make simple arrays and std::vectors extremely fast data structures, far faster than compared to 80s or 90s computers anyway. A lot of optimization advice from the past has become outdated because of the evolution of CPUs.
So its important to bring up these changes in discussion, from time to time, to remind others to restudy computers. Things change.
Hmmm... I argue that the hash is nearly free actually.
An unordered_map traversal is probably DDR4 latency bound. That's ~50-nanoseconds (200 clock ticks) per access. What's the CPU going to do in that time?
Well, spending 10 to 20 clock ticks on a typical hash algorithm is fine. Then it will wait the other 180 clock ticks for RAM. If you got hyperthreading, maybe the CPU will go to another thread and do meaningful work while waiting for RAM... but... I think you get the gist.
Even IF the hash were free, the CPU is waiting for RAM anyway. So you got plenty of time to make that hash worthwhile. Even an integer division/modulo operator (worst case ~80 clock ticks) can fit in there while waiting for RAM, with plenty of room to spare.
I guess if everything was in L1 cache, the story is a bit different. A lot of "depends", depends on the data, the access frequency, etc. etc.
If you can't guarantee n is small, I think it is entirely sensible to use a dictionary/hash table instead of looping over an array or list. As long as n is small the overhead is probably irrelevant, and it'll prevent surprises if n gets large as you said. And if the difference actually matters, you get back to rule 1 and 2 and measure first anyway.
On the other hand, the idea that one might be setting traps is slightly weird... if you _know_ 90% that n will be large then pick an algorithm that's efficient (and since it's easy to implement, it's a win-win). If n is always going to be small, then does the choice really matter?
If you are writing the code yourself then the maintenance costs of everyone else after you trying to understand it makes it wrong. However most programming languages have generic programing features such that you can just use an algorithm and so you aren't writing either the algorithm. In that case the code for the fast hash is equal to the O(n^2) code and so of course you select the faster one as a application of don't prematurely pessimise your code rule. If your programing language doesn't already have built in generic algorihtms for your data, then you are using the wrong language (unless your job is to write the generic algorithms for the language in which case this doesn't apply because you can assume your algorithm will be used in a performance critical part at some time)
If I know beforehand we'll handle a lot of data, I can pick something fast and complex to begin with, but that effort is probably mostly a waste.
- Start with stupid code built on smart data (after Fred Brooks)
- When in doubt use brute force (Ken Thompson)
- Premature optimization is the root of all evil (Tony Hoare)
"Focus on the Nouns, not the Verbs"
:O
Epiphany
Now it does make sense when you introduce an entire constraint library instead of looping over 3-4 variables with a small search space. But again, you know it is a small search space. You know you don't have to optimize it.
I really don't get these rules.
Edit: Go ahead and roast me, but keep in mind I've probably been there and back.
Just this evening I came across some code which pasted an image on top of a blue background (in Go) that set every individual pixel to the background, then got every pixel from the source and set the corresponding pixel of the destination to that colour. I figured it'd be quicker to paste the source onto the destination with `image/draw`.
Turns out, if you're using NRGBA images, it's 40-50% slower. That's definitely an "obvious optimisation" that was proven wrong by measurement.
(If you're using RGBA images, though, the pasting method is 300% faster. Because obviously.)