New Grad vs. Senior Dev
ericlippert.com
ericlippert.com
That gave me a chance to explain the testing and refactoring cost that would come with changing python versions, and how the benefits to users would be almost zero. And then at some point one of the new juniors said, "hey, there's a lot of filesystem performance improvements and readability improvements (f-strings) in 3. I can get the test/refactor done in a month, and I think it's a net positive." They were right, and now we're on python3.
So, sometimes we all learn something.
Long story short, one year and a half passes and the mvp is still not finished, the architect leaves the company and some poor guys (from an outsourcing company) are still going at it with the same architecture.
Having heard mainly "nothing but guff" justifications for microservices in non-enormous orgs, I think (as usual, and some law I forget dictates) the latter is more likely.
Distributed logging:hard
Distributed debugging: hard
Distributed versioning: hard
Distributed transactions: very hard
And on 95% of projects these problems aren't worth solving for the benefits of microservices.
And especially with a team of 4 or 5 developers.
A lot of stuff in software engineering is just driven by what's popular, not what's needed.
In the web world, if you have less than 10s of millions of daily users, as long as you design an application that holds no state itself, the architecture is usually more important for scaling your team and the size of your codebase rather than the number of users you can handle.
It's priceless to see their expressions when senior mgmt. and business owners finally find out.
Given that, once you have more than one service in your architecture, you cannot coordinate transactions across the distinct storage mechanisms - they are distinct databases and are therefore subject to the CAP theorem and other complexities of distributed computing.
Of course, nothing's stopping you from putting all your features into one service to avoid this thorny problem. It's a wise approach for most of us.
When you do that you have a monolithic architecture, not a microservice one.
I don't follow your reasoning. I mean, the ACID vs BASE problem you described is extensively covered in pretty much any microservices 101 course or even MOOC, along other basic microservices tradeoffs such as the distributed tax and ways to mitigate or eliminate these issues like going with bounded contexts. Why do you believe this is a mystery that no one is aware of?
And all I can think about is "Hey, you could build this with a small team as a simple monolith and with proper caching you could probably run this on one or two raspberry pi, that is the amount of power you actually need here".
Don't get me wrong I do think they absolutely have their place and in other parts of the company we have much larger software development projects and they are absolutely making great use of Microservice architectures and Kubernetes and is getting a lot out of it. But that is 100+ teams building a product portfolio together.
If your product has a global audience who needs to CRUD stuff, caching and a single raspberry pi won't get you very far in the game.
If it's just an intranet stuff with a few hundred users and your front-end isn't very chatty then you're right, you don't need much.
One could argue that you need the reproducible dev environments and CI to be solved BEFORE even start using Kubernetes.
So out-of-the-box support for blue/green deployments and fully versioned deployment history with trivial undo/rollbacks are of no advantage to you?
> Reproducible dev environments and CI you can get easily without Kubernetes, without having to add complex solutions for logging, profiling and other introspection tools.
I'd like to hear what you personally believe is a better alternative to kubernetes.
And by the way, Kubernetes does not support not requires distributed tracing tools not "logging, profiling, and other introspection tools". That's somethings entirely different and separate, and something that you only use if for some reason you really want to and make it your point to go out of your way to adopt and use.
In fact, distributed tracing is only a thing not due to kubernetes but due to you operating a distributed system. If you designed a distributed system and get it up and running somewhere else, you still end up with the same challenges and the same requirements.
In 2016, Stack Overflow ran on 11 IIS web servers, but they really only needed 1.
[0] https://news.ycombinator.com/item?id=18496344
[1] https://nickcraver.com/blog/2016/02/17/stack-overflow-the-ar...
At least that is what I often think, when I hear people describing micro-services. If there is no data sharing between computations, then the problem is embarrassingly parallel [1] and thus easy to scale. The problem is not the monolith, it is the data sharing, which micro-services only solve if each service owns it’s own data. To be fair people advocating micro-services also often argue that they should own their data, but in quite a few of the instances I’ve heard described, this is not the case.
Strong module boundaries
Independent deployment
Technology diversity
And with a single database you severely limit the benefits from strong module boundaries and independent deployments. Suddenly you have to synchronize deployments, and anyone can pull or change any data in the database which now must be enforced with discipline which gets you into the same boat as a monolith.
Only worse because you don't have tools (IDEs, linters, etc) telling you what parts of the code have to be changed.
A calls B, C, and D. B puts an item on a queue that eventually causes a call to C. Both B and C call D. Which D call is slow/erroring? Is it the AD, the ABD, the ABqCD, or the ACD call?
I have a project where only 3 developers work; we re designed it to be cqrs. I was sceptical at first - I especially don't like some of the boilerplate it creates - but I'm now sold on it by combining it with the mediator pattern, now I can have validation, logging and performance checks on every command and query without repeating code, works like a middleware
And ofc being only 3 devs it would be nuts to have microservices so we are happy with a cqrs monolith
This way, if an implementation path doesn't worth the effort, just drop it and don't spend dozens of months with a full rewrite and realizing that you are spending a week to implement a CRUD for a simple entity. It's even worse when devs are new to the project and have no knowledge of the domain; I've been burned by this, never again.
At the end of the day, we are not in the research field, we are payed to fix business problems, to deliver something that brings a business value. Yeah, it's nice to write some really complex software that would make the system a lot more optimized, but we have to deliver it before our bosses are getting tired of excuses and pull the plug. Learned that the hard way.
Allaire were there with a workable solution, before jsp, php, (?) asp. Iirc only perl was serious competition.
I still sometimes see lotus notes '.nsf' links
Edit: Also want to add software installation files are very important to keep offline as well.
that's how you earn your thousand yard stare
It is very rare to see a codebase several years old that can be fairly describe as "in good shape".
If they have “the answer” but don’t understand the “why we are here today” their decisions should be, at the very least, suspect.
I am a senior dev and I always try to push new ideas to my team lead (senior dev and older than me), but all I get is blatant criticism because he says "I've tested it and didn't like it). A clear example: refusing to move to Spring Boot and still staying on the dead horse JavaEE, which got more complicated and fragmented than ever since Java 9
Parts of the problem get worse as time goes on. (more code to convert, more complexity, more test cases, EOL for the python2 version of some lib, more user data, blah)
Parts of the solution get easier as time goes on. (easier syntax sugars, better testing frameworks, infrastructure abstracted easier, remove dead features, etc)
Parts of the desired solution become less relevant as time goes on. (Why not use golang or node or elixir, php and python are so dated!)...
Just because last year was a bad time for the upgrade doesn't mean today is. Knowing how to get these things done at the right time by the right people for the business is what separates a great engineering manager from one that is just "shipping features and bug fixes".
Your experience sounds great, but it also sounds like we don't see that level of middle-ground pragmatism enough - it's either "no, we have to stay using PHP4.3.3 because it's what I know" or "we have to rebuild to cloud micro services to be able to scale infinitely without being restricted by schedules or budgets".
Definitely have been one or two refactors that have been shutdown. We've been lucky to have clients that give us the freedom to retire some technical debt/risk instead of just churning out features. And we're small enough (200 kLoc approx, 6 devs) that full codebase refactors are still doable.
"I'll rewrite it in a week!"
A core component of making great decisions is understanding the rationale behind previous decisions. If we don’t understand how we got “here,” we run the risk of making things much worse.
So you helped the new dev understand the current lay of the land. They listened, then suggested an improvement based on their new understanding. You agreed and together improved the code.
Sometimes I just can't find out (the guy who wrote it is gone, nobody knows) then I have to just do it the way I think it should be, test ... then I find out why, sometimes spectacularly ;)
It's a pure cost/benefit analysis question. Switching has some cost, and some benefits, and you have to decide which are likely greater.
This has not been my experience, even within the past few years on occasion.
Possible answers:
• Legit technical or cost/benefit reasons
• Probably a good idea but no budget/time for the effort
• Somebody already tried and it was a disaster
• No manager has been stupid enough to humor the devs
Mostly devs want to go away from the old and increasingly creaky thing. And when the cost / benefit ration is finally right (that is, it's creaking loud enough and slows things down much enough), the move hopefully happens.
Most devs are not reasonable.
Anecdotes are by definition anecdotal; I am not promoting an anti-science position by relating a personal anecdote and I resent the statement that I am doing so.
If you'd like to write a blog article that promotes scientific thinking, I strongly encourage you to do so.
Only if you just fix whatever breaks in the upgrade and never use the features in Python 3. The static typing benefits alone should either make your software more reliable (benefit to users) or speed up feature delivery (benefit to users).
Some people will use any excuse to not have to change. Once in a while it's because they are genuinely too busy, but that just signals other issues.
I learn a lot from them, because I'm asking them to do a lot, and they also learn from me when I review the code or advise them on an alternative. They push back a lot and that's what I want. I want my reports to prove me wrong and show me better.
This approach means that if I really have to put my foot down or enforce something, then there is enough mutual trust to allow that to happen.
How does this benefit the user again? Could the junior have been working on that spa for marketing? That's one months salary..
A junior tried to advocate for browser test. Senior from the developer productivity team said browser test couldn't possibly be made non-flaky.
I didn't know how to advice the junior because I agreed with them.
Saying "something is infeasible/costly" is like a blanket statement. There was no way to quantify that.
On a flip side, we couldn't justify the impact of browser test either. But I felt, at the time, we should've been biased to implement every type of tests (than not) since there are only 3 types: backend, JS, and browser.
Akin to your example, it was the same argument "X is costly/infeasible". Your example doesn't have any issue because everyone probably agrees with it. But being infeasible to setup browser test sounds strange.
Also, if we don't plant the tree now, then when?
1) Keep a set of a few thousand URLs to diff, add new ones as needed.
2) Use a tool that can request these URLs from two versions of your app, make a side-by-side visual diff of the resulting pages, and present a report of any differences.
3) Before committing any change to your codebase, run that tool automatically on the whole URL set, comparing the new version of the app with the current version. Then the committer should review the diff report manually and it gets attached to the commit history.
This way, adding coverage for new functionality is easy (you just add some URLs to a text file) and the whole thing runs fast. And it catches all kinds of problems. Not just UI bugs where some bit of CSS messes up something unrelated, but also you can run the frontend diff after making any backend change and it will catch problems as well. It won't solve all your testing needs, but it covers a lot of ground cheaply, so you can concentrate the custom testing code where it's actually needed.
If the test is slow and hard to deflake, then let's add it slowly. Maybe only test the critical path. There are ways to manage it, instead of discarding it entirely.
As a manager I always hated these situations, firstly, because the problem the juinor dev is trying to fix is almost never as productive as "Hey let's upgrade to python3", it's more like "Hey let's migrate to this specific version of this specific tool that I happened to use on one of my pet projects" or "I've got this incredibly ambitious plan to change everything about this peice of code you gave me to work on" (You're only looking at that code at all because it's simple, relatively unimportant and we're trying to ease you into the team).
The problem is if it is the junior dev whose wrong you're now going to start seeing all these new issues because you've taken someone whose intuition isn't quite there yet and given them something incredibly complex to do. That's when you get into work 2 weeks later and find their mega-commit changing 2,395 files changing tabs to spaces and auto-modifying everything to camel case. Taking a chance on that intern's pet project means a lot of support from others in the team.
I'm not sure sure why, but CS courses and interview questions mostly focus on _asymptotic complexity_ and usually forget to take into consideration the complexity for "little values of n". And funnily enough, in real life n never goes to infinity!
In a strict sense big O notation only cares about what happens when n goes to infinity. The algorithm could behave in any way up to numbers unimaginable (like TREE(3)) but still, its big O wouldn't change.
Maybe what is missing to those "new grad" is a felling of real world data, and how a computer behave in the real world (with caches, latencies, optimised instructions etc...) not just having an ideal computer model in their mind when they design algorithms.
I think it's fine that the academic courses focus a bit more on what's better in theory than in practice, because there are always caveats to "in practice"; the person who writes the special-purpose genomics libraries was also once a new grad.
Of course this depends on your language. A c programmer will have a different mental model than a python one.
Performance is always important. Especially for consumer applications, where your software will probably need to run alongside many other processes each competing for resources.
I disagree. Software has gotten slower over time because we are adding more fluff to it (SDK’s, libraries, electron, GUI animations, web interactions, frameworks, etc). Not because the developers are failing to focus on code optimizations.
For example, according to the theory, a hash table is much better suited for key lookup and random additions than a vector. In practice, if you're storing a couple hundred elements, a flat array (with objects stored directly) will be faster because of data locality. If your problems are mostly "do a lot of small N ops" and not "do some large N ops", then big O analysis isn't all that useful anymore.
You could make a hash table with a constant time lookup, but the hash takes 1 hour. Big oh only tells you how it scales, not it's performance (runtime).
Also - if I see someone try to use a linked list for an enormous data structure again.... Wow it does not scale worth crap because it turns out that the hardware is actually important, and contiguous memory is amazing.
I agree, but in a way opposite to what you intended. An experienced developer[0] should be able to look at a situation like this and realize that few more minutes of focus can yield a better (array-based vs. list-based) implementation[1]. There are no downsides to that (arrays were only slightly less convenient in that case, syntax-wise), improvements occur regardless of scale. The list-based solution was a bad one at the scale it was originally written for handling.
I believe a hallmark of an experienced developer is writing performant code from the get-go; this is accomplished by not making stupid mistakes like this, and it costs pretty much nothing in terms of coding time or code complexity. All it takes is a little knowledge and caring about the product's performance.
--
[0] - I hesitate to use the word "senior", because to me, whether it means anything depends on the company one works in. In many, a "senior" developer is just the one that came before all the "junior" hires, and it doesn't matter that that developer is a fresh bootcamp graduate. And once you can put "senior X developer" on your CV, it's likely your next job will give you seniorship immediately as well.
[1] - and an extra few more minutes would give an implementation that doesn't allocate new memory unnecessarily - also a huge performance win.
Or a hashmap to prepare 3 variables to pass to Json serialization.
Curious - what would be your solution? Just creating the json directly as strings / bytes?
It occurs to me that I don't know whether any of the major dynamic language implementations with maps/dicts/hashes as a central data structure use a similar approach for very small ones… huh.
Some of us are working in, say, Python. A flat array can outperform at small n, yes, but people overestimate where the tradeoff point is. It's at <5 items:
# A list of [0, 1, 2, 3, 4]
In [10]: linear = list(range(5))
# A hash set, same thing.
In [11]: hashing = set(range(5))
# 44ns / linear search
In [12]: %timeit 3 in linear
44.2 ns ± 0.412 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
# 25ns / hash search!
In [13]: %timeit 3 in hashing
25 ns ± 0.6 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
The hash set outperforms the linear search by nearly 2x, on a list of size 5! (The performance is similar for other common types that end up in hashes, like strings.)"It's Python!", you say. "Too much chasing of pointers to PyObjects destroy the cache!" And yes, they do; but many people are working in high-level languages like Python or Ruby.
But, for those that aren't, if we repeat the above exercise in Rust, yes the tradeoff will move up, but only to ~60 items, not hundreds or low thousands:
test tests::bench_hash_int ... bench: 14 ns/iter (+/- 1)
test tests::bench_linear_int ... bench: 19 ns/iter (+/- 3)
If you're thinking that somehow accessing the middle item each time bestows an unfair advantage to the hash table, randomizing the desired item doesn't help, either: test tests::bench_rng_hash_int ... bench: 19 ns/iter (+/- 2)
test tests::bench_rng_linear_int ... bench: 24 ns/iter (+/- 2)
And looking for an item not in the list is definitely not favorable to the linear search. (It's the worst case.)In my experience, it's almost always easiest to pay mild attention to big O concerns, and just use the appropriate data structure for the problem at hand. Cache effects mattering is either rare (you're writing a RESTful microserving to push cat pictures, a cache isn't going to matter once we hit this mobile devices 20 second network latency!) or highly context dependent (your line of work is always low-level, and these crop up more often, and you're consequently on the lookout for it; I don't think this applies to most of us, however).
The code used, in case you wish to find fault with it: https://github.com/thanatos/hash-vs-linear
I ran your Python test on my machine and the hash set was faster in every case: 10x faster at size 50, 2x faster at size 5, 1.3x faster at size 3.
I don't know how to test it properly, but I tried looping over both the initialisation and a single search, and the hash sets were much slower, so much so that hash sets trailed lists by microseconds at size 10000. In retrospect, making those data structures unsurprisingly dominated running time and the test kind of lost all meaning. It was clear, however, that it wouldn't take many searches for the hash sets to win, search times for lists were going through the roof.
If you want to see real-world DDR speeds figure out what algorithms do linear reads.
So, grads listen to their CS professors and that’s what they know. It’s not until they get lectures from greybeard software engineers that they learn about the reality algos and not just the idealized algos.
Take a look at Real-time Collision Detection[1]. I takes a great look at both algorithmic complexity and cache awareness. That's how it should be done.
If your linked list nodes are all allocated sequentially in memory then it'd only be 2x as slow as an array of 64 bit integers.
But maybe it's not fair to call sequentially allocated linked list a "trivial linked list".
1) Do you think about cache at all or is it just something you heard mentioned as important that one time?
2) It's a good lead-in to discussing the effects of cache in algorithms. How that conversation goes helps me to understand how that person thinks and discusses complex problems.
A good answer would be "I'm not sure, but probably way, way slower because linked list can point all over memory but arrays cache really well."
An excellent, A+ answer would be "In the best case it might not be too much slower if you have an intrusive linked list is arranged sequentially in memory like an array like onekorg explained. But, in practice most will be 20-200x slower because they are usually implemented as pointers to nodes containing pointers to data and each piece is allocated piecemeal in a already fragmented heap. Uncached memory reads can take 100+ cycles and summing an int is not enough work to hide that even if the CPU speculatively prefetches."
I mainly expect a surprised reaction that they could be so slow and looked forward to the follow-up discussion.
It's not that Big O isn't useful - it's that it's taught as a set of "proofs" which somehow make it appear objective and "correct", when in reality performance is at least as dependent on cache architecture, median size-of-n, memory bandwidth, and other implementation details.
Anyone who graduates CS without having been taught this very forcefully - preferably during a practical project - should be refunded at least some of their course fees.
My A+ answer is "My guess is [x] but instead of speculating we can create a test to discover the performance. [Describes test]."
Koala_man above says:
> I wrote a benchmark and found the difference in this case to be 3x-3.5x.
The actual number depends on a lot of things, of course (language, architecture, test methodology...), but it is possible that your 20-200x A+ answer is incorrect.
200x can be a reasonable outcome. So can be 3x in other conditions.
As a rule of thumb I now consider that a completely random memory access is on the order of accessing 1000 sequential bytes.
I'm guessing it's not CRUD apps.
Imagine for the array it's 1 CPU instruction to load a value, 1 to load the next value, 1 to add them, and one to store the result, that would be 4 instructions per sum; ideally the array would stream into the CPU after a single main memory lookup delay up-front, and then be 4 instructions per pair, summed as fast as the CPU can loop.
The linked list at worst needs an imaginary 1 CPU instruction to load a value, 1 to load the pointer value, 1 to reference the pointer, a delay of 2 seconds to get that value from L1 cache - missed, it's not in cache - 240 seconds stalled waiting for main memory, 1 to add, 1 to store the result. Worst case, >240x slower.
The linked list is not guaranteed to be in contiguous memory, but it might be, so the cache might have the right data in it. The linked list is 50% data, 50% metadata, so the cache is half wasted / can hold half as much data, and if the linked list is coming in from a big memory read quickly, half the bandwidth is carrying pointer addresses not data, so the throughput is halved for that, too, and the processor cycles were already able to happen much faster than the main memory bus max speed. If it's not contiguous memory, you don't know in advance where it is to request all the right memory areas at once - not until you read sequentially to the last item pointer and find there are no more.
Maybe if they are both small, both in contiguous memory and go into Level 1 cache after a single main memory delay, it could be only ~2x time, but the more data there is overall, the more chance the linked list will bust the cache or be discontinuous in memory. And on the plain array side, it might be possible to speed up with SIMD/SSE instructions to spend fewer cycles adding and storing per element, which the linked list approach might not be amenable to at all[2], then best case might be ~4x slower, worst case ~500x slower.
[1] https://www.prowesscorp.com/computer-latency-at-a-human-scal...
[2] https://stackoverflow.com/questions/10930595/sse-instruction...
Does anyone have a good textbook suggestion for cache/simd aware algorithm design? I've seen plenty of papers that cover single examples but never something the scope of a book.
Since all the students will merely be spheres of equal density, that shouldn’t matter much.
Not in its current form, and not if you define "computers" with a sufficiently broad net.
(Or broad loom, tipping a hat to Jacquard... )
No, various bits and pieces of it did, but not the whole, coherent field, which is motivated by the existence of computers.
A fine concrete example of this is the Coppersmith–Winograd algorithm (and its derivatives), a matrix multiplication algorithm with impressive complexity properties, but which in practice always loses to the Strassen algorithm, despite Strassen's inferior complexity. [0][1][2]
(Aside: the Strassen algorithm is pretty mind-bending, but also easily shown. If you've got 22 minutes spare, there's a good explanation of it on YouTube. Perhaps there's a more dense source elsewhere. [3])
> It’s not until they get lectures from greybeard software engineers that they learn about the reality algos and not just the idealized algos.
To mirror what some others are saying here, students should also be taught the realities of cache behaviour, SIMD-friendliness, branch prediction, multi-threaded programming, real-time constraints, hardware acceleration, etc.
[0] https://en.wikipedia.org/wiki/Coppersmith%E2%80%93Winograd_a...
[1] https://en.wikipedia.org/wiki/Strassen_algorithm
[2] https://en.wikipedia.org/wiki/Computational_complexity_of_ma...
Although apparently that threshold can be lowered (http://jianyuhuang.com/papers/sc16.pdf), but even then it's a matrix that's several hundred columns by several hundred rows large.
Some CS classes explicitly use Strassen to teach the realities of asymptotic vs wall-clock time complexity, challenging students to come up with a hybrid matrix multiplication algorithm that performs the fastest and switches at the best thresholds of matrix size.
Which would have the positive knock-on effect of the textbook being sufficiently obsolete every year or so that the students could no longer trade it in for credit, saving the bookstores money!
More seriously, that knowledge (at least once you attach numbers to it) has a shelf life, and not a very long one. Teaching big-O analysis means the knowledge is timeless, which any good theoretical knowledge is, and moving more towards practice would force the professors to keep on top of the state of the art, and the state of the mainstream, of hardware design in addition to everything else they're doing.
I wasn't very clear on that point, but didn't mean to suggest it be the same textbook. These other topics deserve courses and books of their own. The algorithms lecturer should be careful to emphasise the limitations of complexity theory though.
> that knowledge (at least once you attach numbers to it) has a shelf life, and not a very long one
Plenty of long-lived principles to be learned there, even if the particulars change over time. Caches are still going to be around in 10 years time.
That's naive. E.g. SIMD is here since a good time and going to stay. So are GPGPU, with now quite similar architectures for tons of chips.
And Computer Science can actually be about science for real computers, and computers are not 8086 nor PDP11 anymore, and have never been a turing machine. So there actually is some existing generic CS and ongoing research that cares about cache effects and so over. Maybe it is applied CS if you want, and some kind of pure CS should not care about that, but I really don't see what should be the criteria to decide which is what anyway, so IMO there should not be any (but I do not mean that all research should care about e.g. cache effects, just that it is not really useful to attempt to distinguish between those which do and those which don't).
We don't teach advanced math by only showing what was done at e.g. the beginning of algebra. Neither should we stick to only basic subjects in computer science.
They are in many places I'm aware of. At least, as an EE (at Stanford, but I've heard MIT and several others do the same), I had to take a digital system design class, but the majority of the class was spent on performance engineering. In fact, the very first (actual) project of the class was to take a 10-line piece of C code, which applies a simple filter in real time to a video, and make it performant. The initial code runs at around .5 FPS.
Our resulting performant code was, of course, many times larger (I think it might have been ~150 lines), but it ran incredibly quickly (110 FPS, iirc), by doing crazy compiler tricks and often calling ASM from within the C code, even though the asymptotic (big O) performance was exactly the same.
For context, this is not just a digital systems thing (my work is in mathematical optimization theory and my undergrad was in photonics and physics), but I do know that this class is not a requirement for CS since it's potentially too hardware oriented. The classes exist, but I'm not sure people are taking them.
Yes, sure. I hadn't meant to imply otherwise. The pure-algorithms lecturer needn't cover these other topics in detail in their course, but should be careful to emphasise the uses and limitations of complexity theory.
> this class is not a requirement for CS since it's potentially too hardware oriented
I don't see the sense in this. Computer scientists publish work on applying GPU acceleration, as they should - that's not electronic engineering work they're doing. We could quibble about whether it's computer science of software engineering.
I agree, I'm not sure why this is the case either, just my idea as to why it may not be a requirement. (Some part of the class does involve writing a good chunk of a 5-stage RISC processor based on MIPS, but this was still relatively straightforward with just a basic understanding of digital logic.)
Is an approach dependent on swathes of training data truly scalable if it doesn’t work for the first n attempts?
Taking an extra 30 minutes to an hour or longer to optimize and test for large inputs that will realistically never exist is a waste of time and money.
If you feel that the value of N might, in some strange and rare combination of success and changed requirements, exceed the expected amount, add a check for the lowest value that may signify a problem and throw a warning.
if N > 1000:
debug.warn("N count of %d may be too large for existing algorithm. Consider optimizing.", N)
Leave it at that and get on to more important things.It is more about peace of mind. By using the more efficient algorithm (by asymptotic complexity), I know that my code won't become a bottleneck. That's like using "size_t" instead of "int" in C. I know my array will not exceed 4GB in any practical application, but by using size_t, I know it won't crash if it happens one day. One less thing to worry about.
Almost all well designed libraries use hybrid approaches, switching from an algorithm optimized for low level efficiently for low N to a theoretically more efficient algorithm for high N. For example a sorting algorithm can go from insertion sort (good for low N) to quicksort (very efficient most cases) to merge sort (guaranteed nlog(n), highly parallelizable).
After a few more years working on real-world code, I understood that it's usually better to choose the algorithm with better asymptotic complexity, anyway.
Like, sure, my O(n) algorithm will be 10x slower than your O(n^2) algorithm for small n. But users aren't going to notice a few microseconds. Users ARE going to notice my O(n) algorithm being 100000x faster for large n, when it's the difference between milliseconds and minutes.
Once I was asked to look for one project to see that if there is any room for improvement to speed up the program. After I profiled the program with the test data, I saw that program was severely affected by a "size" method call on a lock-free concurrent list. Since the data structure is lock free, size method is not a constant time operation and calling it in a large list takes too much time. It was just there to print some kind of statistics, I changed the code so that it is called only necessary not every time some operation occurs. This immediately made program 2-3 times faster.
There were also some parts I changed with some algorithms with less algorithmic complexith to make it faster. Overall, I made the program 6x faster. So sometimes you need to use fancy algorithms, sometimes you just need to change one line of code after profiling.
Correctness and simplicity are almost always things you want to focus on over performance, but there’s still SOME level of simple performance best practices that you want to stick to up front. Similar to the tradeoff between YAGNI and software architecture - if you don’t start with some sort of coherent architecture, your project is gonna descend into a spaghetti mess. Ignore simple performance best practices and you’ll spend way too much time fighting performance issues.
Right, it's a balance good developers know how to tread. Obviously if you can use a set instead of a list and it's one line change, go for it. But as the meme in OPs post, if you're gonna USE SEGMENT TREES and all, then it better be worth the amount of complexity and time you're putting into it.
Part of what makes a good engineer is being able to quickly tell where it's worth optimizing and where it isn't. Or as the meme says, when nested loop goes BRRRRRR.
But a 50x win that, as you note, goes from "let's have lunch" to "let's change this one thing ten times and re-do the analysis to see which gets us the best results" is a huge game changer; it's not just that it saves time, it's that it makes possible new ways to work with data.
A little bit after I have made the change, an user filed a bug [2] to the library about slow behavior when you passed a large amount of data to the library. They were using the public release instead of git master, so my code wasn't at fault thankfully. Due to the knowledge I attained from doing the change, I could quickly confirm that precisely this slow part was cause for the performance slowdown, and was able to file a PR to reduce the library's complexity from quadratic to linear [3].
It wasn't very complicated from the algorithmic point of view, I only had to create a lookup structure, but the lookup structure had to be created in a certain way so that the library still behaved the same way as tested by the test suite. It also had to support things like duplicates as the original implementation also supported them, as a very important feature in fact (toml arrays).
[1]: https://github.com/alexcrichton/toml-rs/pull/333
Day 1: do some basic hoisting. O(n^3) => O(n^2). Tokenization times for a 10k line file go from ~15s to 500ms. Sweet.
Days 2-30 [1]: ideate, develop, bug fix, iterate on, etc, a novel (to me) data-structure to speed up the program even more. O(n^2) => O(n x log(n)) (expected). Great! Runtime on 10k line file went from 500ms to maybe 300ms. Oooops.
But hey, all the people working in 500k line files must really love the couple seconds my month of toiling (more importantly, my month of not doing other, more impactful things) saved them.
Learned a lot from that experience, and writing this out now I can see how that impacted engineering decisions I make to this day. I suppose thats the real point of an internship, so time well spent, in a way.
[1] It probably wasn't actually a month, but certainly a significant chunk of the internship.
This stuff matters. These couple seconds per operation may very well be a difference between being able to open a 100k+ LOC file in the same editor you're doing your other work in, vs. giving up in frustration and looking for something else that can handle large files. Large files happen surprisingly often (in particular: log files, data dumps, machine-generated code). A "month of toiling" like this may immediately enable new use cases.
Lessons upon lessons :)
If this subject in particular interests you, we did a lot of work in the C# lexer/parser so that once the file is lexed, it only re-lexes the tokens which changed on every edit. It also does fun stuff like the syntax colourizer only runs on code that's actually on the screen. Getting every operation that depends on the lex/parse of code in the editor down to running in much less than 30ms so that it would not slow down keystrokes was a huge amount of work.
So we go from a data structure representing "original text plus edit" to a data structure representing "original lex plus changes". Now we have the information we need to do the same to the parse tree, which has also been stored. Given the set of tokens in the program which changed, and knowledge of where the textual boundaries are of every parse node, we can restrict the re-parse to the affected syntax tree spine. In the example given, we know that we've still got, say, an array index list but the contents of the list need to be re-parsed, so we re-start the parser on the left bracket.
The algorithm that does this is called "the blender", and reading that code makes my brain feel like it is in a blender. The code was written by Neal Gafter and based on his PhD thesis in incremental parser theory.
The source code is available on github; do a search for "roslyn" and you'll find it.
And the go extension is a thin wrapper around standard go tooling, we weren’t tokenizing ourselves just converting between their tokens and ones we could process; a large part of that was converting from byte offsets to UTC-8 character offsets.
You probably know what you’re doing, just curious why these numbers seem to be off so much to what I would expect. What approach did you use for tokenization if I may ask?
As mentioned in another comment:
The go extension is a thin wrapper around standard go tooling, we weren’t tokenizing ourselves just converting between their tokens and ones we could process; a large part of that was converting from byte offsets to UTC-8 character offsets.
The quadratic behavior was a bug caused by reconverting segments over and over again instead of converting deltas between previously converted subsegments.
It seems that there is a spectrum of skills an engineer could excel at: programming, infrastructure, managing, planning, etc. I’ve known senior engineers who only excel at a particular skill. I’ve also known senior engineers who are moderately good at many but not particularly good at one.
In my experience the only difference between a senior and non-senior is that the senior’s distribution of skills makes them more effective.
I’ve also seen senior engineers change roles laterally and become more or less effective, yet still maintain the senior title.
I've seen it a day or two ago. Can't find the picture anywhere now (I've seen it in some group chat). Anyway, beyond the words quoted at the beginning of this article, the meme's "nested loops go brrr" had a picture of a triple-nested loop using Active Record to do some simple database operations.
To which the correct response is: "it's a 'senior developer' in an industry where you get called a 'senior' after a total of 3 years of experience doing programming; don't do stupid shit like this, just use SQL like it's meant to".
It was based on a similar story the one in OPs blogpost. At my first job I used to work with some really talented fresh grads that wanted to show off their algorithms skills and ended up over-engineering stuff.
One of them implemented a trie and stored it in SQL lite to implement some string autocomplete where the number of strings was something like 100.
The other implemented a 2D segment tree for doing some grid updates where the size of the grid was small. This inspired the first part of the meme. Segment trees and sqrt decomposition are topics that are popular at programming contests and nowhere else really.
Regarding the triple nested loop, I just wrote the simplest pseudocode that represents nested loops, not necessarily something a "senior" developer would write.
Sorry for being harsh, I got triggered by that code inside the printer, because I've dealt with a lot of dumb "I don't know how SQL joins work, so I'll use my ORM to do it and filter the data in code" cases early in my career, and I have sort of an allergy to that now.
Nested for loops go brrrrrrrrrrrr, munching squares go bweep bweep bwweeeep bwweeeep bwweeeep bwweeeep bwwwweeeeeeep bwwwweeeeeeep bwwwweeeeeeep bwwwweeeeeeep bweep bweep bweep bweep...
https://www.youtube.com/watch?v=V4oRHv-Svwc
Life goes shlup shlup shlup shlup shlup...
https://www.youtube.com/watch?v=hB78NXH77s4
If they use any Don Martin sound effects, I hire them on the spot.
https://www.madcoversite.com/dmd-alphabetical.html
>CHK CHK CHA-GONK BRBBRBBRING! -- Man's Eyes Being Poked Like A Cash Registers' Keys And Jaw Popping Open Like A Till Drawer -- Mad #61, Mar 1961, Page 18 -- Kitzel's Department Store
Some companies like GAFAM probably put too large focus onto algorithmic questions, but they can afford to lose otherwise good engineers who are bad at algorithmic questions. They need something to filter the masses of applicants they receive.
Understanding > Knowledge
It's that simple.
Deadlines > Ideals.
> In theory, theory and reality are the same.
> In reality, they're different.
The New Grad had knowledge. The Senior Dev had Wisdom.
Wisdom > Knowledge.
Put another way, knowledge is the nodes. Understanding is grasping the connections. Understanding is the higher power. Understanding is where the magic happens.
Wisdom? Wisdom is next level understanding. It's the maturity of developing connection within the connections.
Having an intricate understanding how things are connected, doesn't necessarily mean that one feels compelled to make it better.
Sometimes, "understanding" actually begets a cynical form of apathy. The whole world is chaotic and full of holes and injustices, so why bother trying to do the right thing, if it won't make a difference in the "big picture"?
Sometimes, knowledge (of your corner of the graph) with a hefty dose of misunderstanding (of the nodes and edges you're about to be traipsing down) is what's needed to most successfully face the challenges at hand. One name for this is "idealism", but without the negative connotation it is sometimes shipped in.
This is more what I was getting at with the whole knowledge + inexperience bit, if this makes sense.
Hear hear. CS students fresh out of school seems to carry the notion that they are superior to people who haven't studied CS, even though those people been in the industry for many years. Once the ex-students are now working in a professional environment with deadlines and stakeholders, it takes them a couple of months before they realize they have to actually learn how to work as a software engineer now, and that has nothing to do with CS.
There's so much stress and attention given to complexity theory for software engineers, to the point that people will cram for hours to make it through FAANG interviews. I understand that it's important and it's just something that everyone has to go through... but data structure and algorithm performance is a Wikipedia search away, and then you choose the appropriate STL container and move on with your life. The same can't be said for gaining an understanding of modern processors.
I'm not saying that one is more important than the other or vice-versa, I'm just saying that it seems wrong to me that not knowing algorithms and data structures can break an interview, whereas not knowing hardware is virtually a non-factor.
The big take away for me is this: if you're not benchmarking, you're cargo culting.
The big iron was a product of large-data-volume business problems - payroll, airline reservations, insurance quotes, credit cards, catalog order stats.
But comp-sci mostly put FLOPS ahead of TPS.
hands up who's heard of data flow programming
By the way, usually we use "complexity theory" for a part of theoretical computer science (math) concerned with proving lower bounds (like the most famous P vs. NP). What is required for basic analysis of algorithms (without any fancy methods) is not very hard. Yes, you can search the web like we all do, but first you need to know what are you searching for. Also, we reuse algorithms/data structures but not only in the final product but also in our own new algorightms so you cannot search it.
Regarding interviews, I think the main problem is that many companies and interviewers try to blindly copy the questions (cargo cult) without understanding the point of such interviews. The point IMO is that you evaluate analytic skills of a candidate. Correct/incorrect answer is not everything. You could do the same with math, science problems but algorithms/data structures are closer to programming. Anyway, I feel this is becoming a controversial topic.
The strings.Index code is at https://golang.org/src/strings/strings.go?s=25956:25988#L101... and the internal bytealg package with all the CPU intrinsics is at https://golang.org/src/internal/bytealg/ with index_amd64.{go,s} as the relevant files for x64.
Battle-tested implementations of fundamental things like string searches, sorts, hashtables, allocators, graphics primitives, etc. are often interesting 'cause you find out a lot about what you need (and/or don't need) to work well in practice as well as protect from exploding worst-case scenarios.
But more often than not, I don't, and it's not.
My take on it is that for the most part senior devs are a lot more paranoid on breaking things and don't want to make code changes unless enough people are asking for it and it'll make a notable difference. Even making things strictly faster can break users if they depend on the algorithm being slow (an extreme edge case that you'd usually say is the user's fault anyway, but could nonetheless be relevant in things like optimizing away a spin-wait).
InStr(<this page of 100+ comments>, "docum") = 0
I'm a dev with some grey hair who feels it would have been useful for all that fantastic domain knowledge from Paterson to get documented in a code comment.I'd love to hear if either of them ever went back and did that?
Love that article, it was interesting to read your dissection of code comments.
For anyone else reading it, here's an updated link to the CS file referenced in his blog post (the original moved): https://github.com/dotnet/roslyn/blob/master/src/Compilers/C...
Sadly, most programming jobs seem to primarily involve code bureaucracy rather than the "algorithmic" level.
And for me, the work that solves real-world problems tends to be software engineering (using my own definition), rather than computer science (again, using my own definition), which seems to be more about optimizations.
Gods, it was wonderful!
As I've moved up the ladder and worked on complex enterprise systems, with umpteen integrations, overly-strict SAST systems, enforced 90% test coverage and the like, I seldom feel the "joy of code". It was good to feel it again!
This is also a good reason why, if you're designing a standard library, you really want to have length-prefixed strings instead of null-terminated ones. If you know the length of the strings you're dealing with, you can swap out the algorithm you use, such that you might use brute force for a very small needle, word comparisons if it's exactly 4 bytes long, SSE instructions if it's a word-length multiple, or Boyer-Moore for very long strings.
a - he could've gone out thinking that performance doesn't matter. but it certainly does in a piece of code being used daily by thousands of devs.
b - he could've thought that the simple implementation is faster but missed the fact that skip is implemented in assembly.
c - he could've realized both but missed the why.
and these failure scenarios are likely to happen because this is an intern we're speaking about.
One or two tricks up your sleeve do not matter but repeat this a 100 times which one do you think would be a better programmer?
I think the willingness to challenge authority figures and to be (often) proven wrong and to learn from it is an essential part of becoming better.
Maybe Tim was understanding because he himself challenged people older than him and in-process learned form them.
Advice like "don't reinvent the wheel", "avoid premature optimization", "write boring code" promote exploitation. which is the optimal strategy for older agents.
but for newer agents, a higher level of exploration is needed otherwise they would converge into a suboptimal strategy
It’s not; the compiler is just fairly decent at transforming string manipulation routines.
oh my bad then, from this text it sounded like it was written in assembly.
In the story the senior dev makes a judgment call (that this routine will be used in LOB applications where the worst case is unlikely to appear so the implementation is OK) which is probably correct, especially considering other priorities. And of course senior devs are much better equipped to make this kind of calls than juniors, but they still can and will guess wrong.
> Moreover, Tim explained to me, any solution that involves allocating a table, preprocessing strings, and so on, is going to take longer to do all that stuff than the blazingly-fast-99.9999%-of-the-time brute force algorithm takes to just give you the answer.
That's why a good implementation will dispatch to the best algorithm at runtime!
The narrative would then be that the microseconds you saved all those devs over the years were wiped out when hackers took down your system.
I'm the opposite of this stereotype, and I think there are more like me. Two reasons as to why:
(1) Psychological:
I never had this. As a junior dev, I don't like to optimize because I feel a bit of pain when I need to moderately focus. I can do it, and I've done it quite a lot. In that sense, I've experienced quite a bit of pain in my life. It's something I've learned to live with. And when I have to focus, I prefer to focus all the way, because whether I focus on 50% or 100%, the pain is roughly similar. This leaves me in a state of either being lazy(ish) and wanting to program in a simple and understandable way versus being willing to go as deep into a topic as I would need to and focus on all the details step by step.
When I'm intense, I also am still sympathetic towards simple code because I know that I understand that in both states. I only understand complicated code when I'm focused.
(2) There are enough CS grads that know better:
Also, on another note. Efficiency analysis is simply not taught at university. Parts of it are taught, but they're never connected to real world cases.
For efficiency analysis (note I haven't done any but I've read a thing or two and talk with a friend who is a performance engineer quite regurlarly about it) I think there need to be a few perspectives in check:
1. What is the user's actual behavior.
2. What is the behavior for malicious attackers (if applicable, Intel should do this more, to my knowledge they are doing it more now).
3. How does the compiler translate it to assembly?
4. How does it theoretically look? Specifically: worst-case, best-case, average-case in both theoretical and empirical sense.
Concluding:
I only have one year of experience in software development, where no one cared about performance yet I know better than to look only at the theoretical speed. So I guess I'm a counter example to the stereotype. I'm not alone. Any CS student who reads HN has a high chance of stumbling upon articles like this and will know that performance analysis is not only about calculating the space-time complexity.
In modern code we'd want to do a threat model that considered the consequences of untrusted inputs.
Come to think of it, neither do beginning game-designers. They think that players will play their game as intended.
I like that you're standing still at the malice part as it is becoming seemingly more important every day.
It's an interesting dichotomy because the most practical solution could go either way. Maybe it's looping over a table of 20 customers and the wasted time is microseconds that wouldn't even justify ten minutes of developer thought, or maybe it's looping over a million customers and causing all sorts of capacity problems. Maybe the memetic "senior dev" here knows the former is true, or maybe his "senior" experience was all misplaced and he's clueless.
Goes a fast spinning motor
You're probably thinking in C# or Java; remember that in C the convention is that a zero char ends strings. If the source string is shorter than the query string then the code will encounter a zero char in the source string at the same time as it encounters a non-zero char in the query string, and the inequality will end the loop before the beyond-bounds dereference.
There are other defects; can you find them?
`starts()` looks like it's not checking if len(source) < len(query), but if, say, query="foobar" and source="foo", when i = 3 the line
if (source[i] != query[i])
return false;
will evaluate to if ('\0' != 'b')
return false;
so `starts()` will correctly return false.Always if len(source) < len(query), we'll return false when we get to i=len(source), because source[len(source)] != query[len(source)] as source[len(source)] == '\0' and query[len(source)] != '\0' since len(query) > len(source).
There's also no null handling here, which was a deliberate omission for clarity. In practice, the convention used inside the VB source code is that null string pointers are semantically the same as empty strings, which introduces some complexities.
Edit: As others have stated, the 'out of bounds' exception should be taken care of by the '\0' at the end of strings in C
The trick to knowing we needed to improve it: profiling!
https://randomascii.wordpress.com/2019/12/08/on2-again-now-i...
Is it really so hard to help other people learn, and to accept that the only advantage you have on them is starting earlier?
I think there many things to consider here. For new developers, I'd encourage you to look for those "old guys" who really know their stuff. There's a lot of unmined gold you can discover there if you find the right ones. I think us older developers would do well to imitate the patience and kindness of Tim Paterson more often. I guess what I'm saying is both sides could do with a huge dose of humility. I know at times I've been the youthful dev out to one-up the "old guys", and I've been the senior dev thinking "these kids today" to myself when dealing with those with a lot less experience. And both of those are bad.
Also, there are many times when a young guy fresh out of college spots a problem, finds a great solution, and makes things ten times better by just doing it! If you're in the business for a long time, it can become too easy to be cynical, and lose your enthusiasm.
I think the best thing we can do is try to let the good stuff from both sides rub off on each other.
If only the world were that black-and-white. It took me a long time to realize the habits I learned from my father in this area were toxic. Not everybody got the same upbringing you did, it sounds like yours was more advantageous than mine in that respect.
I agree with the "more than they deserve" mentality, but let's be honest here: it's a struggle.
We've all been through it as new devs, and we'll all help new devs struggle through it as well.
The sr. dev will look at "old" code in horror and be sure the only way to make it perform is to port it to a newer language/framework/architecture.
The grizzled veteran will profile the code, think, and tweak a few lines. No fancy new things or big re-writes needed.
I find that being open to new ideas, and allowing some time to prove them out rather than deciding beforehand, is the best way to find ideas that move the needle.
Senior comes to you with an idea? Great, prototype it, prove it out. Junior comes to you with an idea? Great, prototype it, prove it out.
Of course, you need to allow some time for prototyping.
When I ran teams, I told them part of their job was to spend the last half of Friday (unless emergency) prototyping their ideas and presenting their favorites at some point.
No one works the last half of Friday anyway, unless it's on this.
That’s what my professors drilled into me (I specialized in high performance computing) and it’s served me well.
Also described as: "how to be old, for young people"
That is also true (perhaps less often) for byzantine process related procedures.
Yes, we had strstr in 1994; it was in the 1989 ANSI C standard.
On a very small system, strstr will probably just be naively coded, to save space. On a big system, it might use Knuth-Morrison-Pratt or Boyer-Moore. I don't have to care; by using strstr, that decision is taken care of.
After all, when you come across a problem, that does not contain just 100 chars, it is very helpful to know what you can use to create something that still works with reasonable resources.
abcdefghijklmnopqrstuvwxyz
If the 26th char isn't z, jump along 26 chars! (More complex, and more cache misses, if z is repeated in the search string).That’s not always a good thing, especially on modern hardware. And obviously, the “single instruction” doesn’t mean it’ll take bounded time to execute…
REP SCASB on 1994 (same year as the incident in the article) Intel Pentium would have taken 9 + 4 * n clock cycles [0][1] to go through a string. So scanning 4 chars long string takes 25 clock cycles.
[0]: Agner's instruction tables page 123: https://www.agner.org/optimize/instruction_tables.pdf
[1]: Plus one extra cycle to decode REP prefix, if previous instruction took 1 cycle.
As even nowadays, it would likely depend on the particular algorithm and data set. I'd be surprised if you can't do better than 4 cycles per char for sufficiently long strings. Most likely for short strings, REP SCASB wins due to setup costs. (Actually that article's skipto method would have of course used REP CMPSB, but that's just splitting hairs.)
Remember that even original Pentium could execute up to two instructions per clock. Unless you messed up with those damn U & V pipes. :-)
The hypothetical faster-than-rep solution would need to process data in 32-bit chunks, faux vector style.
Or with real vector style with vectorized instructions?
It makes sense to delegate some of the microoptims to the hardware.
But regular scalar instructions are also optimized like crazy. Write a small loop, and your state of the art microarch might sort of unroll it by using register renaming and speculative execution, so sometimes basically multiple iterations are executed at the same time (and on top of that you sometimes get uOP cache locking, which then improves energy and hyperthreading efficiency).
Yes, REP MOVSB is fast at least on Intel CPUs nowadays.
So to reiterate one aspect of your point, there might be lots of work, written in C, that occurs in response to the page fault in the middle of your "hardware-backed" single instruction. On top of all the other complexities of cache vs memory access etc. that make scanning and copying memory complicated no matter which way you do it.
But probably in the heyday of Visual Basic, and especially the DOS-based BASICs that preceded it which wouldn't have had virtual memory at all, all of this is less of a concern. The story takes place in a simpler time which serves as a plot device to better illustrate the point.
When did I dispute this?
> Besides, the page in question would very likely be already present.
Really? How are you so sure? I guess you can just abandon all notion of virtual memory and mmap then. 'Cause it ain't gonna happen.
> Really? How are you so sure?
It was in a BASIC interpreter. Most of the time string needle in a haystack search is done, haystack is relatively fresh, almost certainly on a page that is present. Might not be in CPU dcache, but that's another matter.
The arrogant prick should have listened to the new grad.
Senior Dev: Linked lists have very many more cache misses than do vectors, and the difference between hitting cache and hitting main memory is such a huge constant factor that for most reasonable list sizes it never makes sense to use a linked list. Use a vector. Checkmate, smug Lisp weenies.
Actually, I think that it scares the hell out of most of the developers that it is so difficult to get a grip on these things. It is so easy to think that there is a simple solution, a grand idea that will fix the problem. I still find myself falling in the trap and this after having developed software for over 30 year. It is the Dunning–Kruger effect over and over again. I guess it more that as a more senior engineer, you have experienced a little more often.
I don't think this is really true. After you've optimized enough code over the years, you start to get a sense for bottlenecks, and your code is usually "fast enough" even on the first try. When it isn't, finding the problem with a profiler is usually pretty straightforward.
The senior developer optimizes a different set of criteria:
1) How hard is it to understand the code and make sure it's correct.
2) How fast the algorithm in practice.
There are several different reasons why the performance of the algorithm in practice is different than the performance in theory. The most obvious reason is big-O notation does not capture lots of details that matter in practice. An L1 cache read and a disk IOP are both treated the same in theory.A second reason is the implementation of a complex algorithm is more likely to be incorrect. In some cases this leads to bugs which you can find with good testing. In other cases, it leads to a performance degradation that you'll only find if you run a profiler.
I one time saw a case where a function for finding the right shard for a given id was too slow. The code needed to find from a list of id ranges, which one a given id fell into. The implementation would sort the id ranges once ahead of time and then run a binary search of the ranges to find the right shard for the id. One engineer took a look at this, realized that we were doing the shard lookups sequentially, and decided to perform the shard lookups in parallel. This made the code faster, but we still would have needed to double the size of our servers in order to provide enough additional CPU to make the code fast enough.
Another engineer hooked the code up into a profiler and made a surprising discovery. It turns out the implementation of the function was subtlety incorrect and it was sorting the id ranges on every call. This happened because the code sorted the id ranges inside of a Scala mapValues function. It turns out that mapValues does not actually map a function over the values of a hash table. It instead returns an object that when you look up a key, it will look up the value in the original hash table, then apply the function[0]. This results in the function being called on every read.
The solution was to replace mapValues with map. This dramatically improved the performance of the system and basically brought the CPU usage of the system down to zero. Notably, it would have been impossible to discover this issue without either knowing the difference between map and mapValues, or by using a profiler.
[0] https://blog.bruchez.name/2013/02/mapmap-vs-mapmapvalues.htm...
Nested for-loops go brrrrrr
lmao! But #truth.Recently I worked on the same type of project as someone with 10 yrs of experience & a CS degree from Stanford.
A few months later, I created a project, and had a manager with a CS degree. However, when I left, that manager was unable to pickup where I left off, and he ended up leaving soon after.
I have less years of experience, but to me, what matters more is the time within that experience which was put into a relevant business model, product built, or past projects. I.e. a Senior Dev with a CS degree, vs. a New Grad with a Business Background & SWE Experience. It's apples to oranges in many cases.
Also, I'd echo another comment here:
>"daxfohl 1 hour ago [-]
> As a senior dev, I wish that I could say I always knew more than my interns, and that all the code that's there is because it was carefully planned to be that way.
>But more often than not, I don't, and it's not. "
On the other hand, sometimes I'm handed a program written by a new grad to maintain/fix/improve, and rapidly determine that it's less work to just chuck it in the bin and start over.
One of the major differentiators between a newbie and an old hand is knowing how to create a piece of software that those who work alongside you or come after you can understand, maintain, and improve.
That's true. What is common, however, is bad runtime performance losing you users and bleeding your money. Not doing dumb things (like using a list where a vector would do), and taking a moment every now and then to go over your product with a profiler and fix the biggest bottlenecks early, can save you a ton of money in cloud bills (it might even turn out that your product actually doesn't need to horizontally scale at all, giving you further reduction-of-complexity benefits). Or you might end up delivering features that were impossible to do with bad performance (see e.g. https://news.ycombinator.com/item?id=22712103).