One time, to solve the problem of "we want floating point numbers that are close to be considered equal" on an object, the programmer decided to take every field of that object (reflectively of course) turn that into a string, and then compare the two strings together.
Another time, in order to convert a LocalDate to our own internal date representation, a dev decided to toString the local date, split on the `-`s to extract the day month year. (Yes, localdate has day, month, year methods).
One time, to convert an int to a double a dev decided to turn that int into a string and then used "Double.parseDouble" to turn that int into a double (I wish I were joking).
All these and more were found through the joys of profiling :D
I once didn’t want to write or add a dependency for a Python deep equality checker so I just sorted, JSONified, and string compared the two. It worked fine for years. It wasn’t performant important code.
But it still keeps me up at night.
I think this one is actually fine. You thought about the solution, considered your options and made a design decision based on the balance of your priorities. That's what engineering is: a bunch of trade-offs solidified into a product.
The situation where someone uses Double.parseDouble(number.toString()) to translate an int into a double is different. There's really no reason to do it like that: it's harder to understand, uses more memory and cpu and is slower. There's no trade-off that makes that line come out as the best decision.
The code I'm most typically fixing for performance isn't some complex algorithm that needs inline assembly and AVX register weaving... No, the kind of code I most often fix for performance is code where someone does `Double.parseDouble(foo.toString())` and turn it into `double bar = (double)foo;`.
Or code where someone does n^2 when they could have had n by using a dictionary/hashmap. Or code where someone uses a dictionary/hashmap instead of using a simple POJO.
It drives me bonkers when someone apparently goes out of their way to write code in the slowest form possible.
It's easy to point to the title piece and assume the right advice to is try to optimize early - but the reality is that the gains mentioned here basically only matter because these libraries are used by millions of developers, and run often.
essentially - don't try to optimize until you have a reason to make that effort worth it. For these libraries - it's worth it.
For the times you're getting asked to fix performance, it's because performance has become a feature that matters enough to have someone ask you to fix it. It's fine to wait until that time to swing back around and pay attention to it. There are thousands of times where that never happens, because the performance was fine taking the scenic route through a string. Plus - that leaves you plenty of low hanging fruit to pick up some easy wins.
---
I think the real story here is not that folks are taking the slow route, I think the story is that the low hanging fruit hasn't been knocked out of these libraries. And really, that goes to show that a lot of software really is supported by a very small number of library authors, who aren't doing it as their day job, for very little in the way of real compensation.
Ex: SVGO is used by 9.1 million people (on github alone) but only has 163 contributors, and only 4 people have more than 10 commits. That means only one in 55,000 users has bothered to add anything. And there's only one real dev for every 2.25 million users. And "users" here also includes entire companies.
Don't make your solution more complex than it needs to be. Start with the simplest solution and you will often get a faster solution as a side effect.
Taking just the two from the comment right above this:
Double.parseDouble(foo.toString())
This will work for basically any possible value/type of foo, assuming foo supports .toString(). It will work for NaN, it will work for infinity, it will work if the value is already a string.It's explicitly avoiding the complexity of having to consider those cases at programming time, in exchange for taking the slow route (code someone else has written to already do that for you).
Simply casting to double will probably work, but not always - worse, the warnings you get, and the edge cases you might need to deal with will vary significantly across languages. Ex: Java will do different things than C++, which will do different things than C#, Python will vary by flavor (CPython and Jython use different underlying types).
Basically - it's saving the programmer a boat-load of complexity (reduces the edge cases down to just the exceptions parseDouble() might throw), in exchange for doing something slow.
---
Take the hashmap example right after the double example. Will a hashmap let you speed up some algorithms by avoiding n2 in favor of n? Sure - at the expense of memory size and longer initialization. In most places that's probably a sane call. Is it always the right call? Nope. Would not be ideal for my arduino - the memory is so limited n2 is unlikely to matter most times, and again - the memory is so limited I'd rather go slow and finish than try to go fast and segfault.
It's also not the right call when the expected length of the data is always going to be small values of n. If n is always going to be less than 5, I'd bet you good money the hash implementation will be slower in most cases.
----
Basically - optimize when optimizing becomes important, because something is slow and it sucks. Until then... slow is smooth and smooth is fast.
A) the example above was explicitly for int -> double, so the fact that it the path through .toString will work for other types is a benefit we won’t get but which has a cost we’ll have to pay regardless.
B) I find it hard to believe that the Double class doesn’t my have a method you could call which will convert an int and catch NaN, Null, and MaxInteger.
> In most places that's probably a sane call. Is it always the right call?
Just as you’d bet that the hash implemention will become slower, I bet you that only 1 in a million people (if that) would write the Double code we’re talking about as the result of thoughtful consideration.
> optimize when optimizing becomes important, because something is slow and it sucks. Until then... slow is smooth and smooth is fast.
Slow is smooth assumes that the slow path you pick is sane.
There’s also a big difference between having to write code that needs optimization, and writing less code to do a job faster.
?????
Show me where the type of foo was defined in that comment... I'll wait.
For all we know it was some god-awful custom bucket/container class that's hiding all sorts of polymorphism from you.
And I’ve known people who make programs unnecessarily slow and painful because of sloppiness.
That’s not to say you can’t endeavour to “do it right the first time” but very regularly that is not a realistic goal.
Good engineering requires understanding the big picture, particularly what matters and doesn’t matter to a business (and how those priorities change over time).
The challenge with software is the underlying "physics" of our world continue to get more powerful year over year (unlike real physics), so "good enough" ends up being sloppier and sloppier as time goes on, and for a lot of applications it doesn't matter.
Of course, until that day where it turns out it does matter. Good developers I think can anticipate some of those cases, and great developers can pick the few that they can sink the time into getting Right (tm) (c) 2022, to get the most bang for their (time) bucks.
“There is no doubt that the holy grail of efficiency leads to abuse. Programmers waste enormous amounts of time thinking about, or worrying about, the speed of noncritical parts of their programs, and these attempts at efficiency actually have a strong negative impact when debugging and maintenance are considered. 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%. A good programmer will not be lulled into complacency by such reasoning, he will be wise to look carefully at the critical code; but only after that code has been identified.”
— From “Structured Programming with go to Statements” (Knuth, 1974),
We used to call that BASIC.
Deep clone and equals on deeply nested, probably heterogenous, object graphs can be challenging. And certainly error prone.
Just beware of any lists which you'll need to sort manually (on both sides) to prevent false negatives :)
They also did that exact same thing parsing dates, except that they flubbed the logic and IIRC had an off-by-one on the month for the constructed Date object.
I have seen this kind of code many years ago when external company was brought to sell us their implementation of CMS on some (even then) outdated Java/js-on-server-before-node-era platform. I was asked to do code review to chcek if their code can be integrated into our codebase.
I was quite unexperienced at that point in time but enough so to notice that there was something off with the code. There was a lot js that looked almost like exercise in obfuscation. Things like doing join, append and split on array to do push - and it was not abstracted into function - this three operations were repeated all over the code. And then I met the programmer - it was ovious after few questions that he did not know what he was doing and his direct manager, who was also present, know nothing about programming (he was more od a business person as he himself proclaimed).
It was as if this company hired the cheapes guy they could find, that on paper looked competent, give him task of implementing CMS, and only looked at results (the CMS actually worked), but no one ever checked the code that was hiding under (and the guy never worked with anyone else and never read any other code besides his!)
It made no sense.
But we were also in a rush to get this app built, so we didn’t push back on this tortured logic. But we were saddled with this technical debt for an embarrassingly long time.
Completely fails for a number big enough to be stringified to 1.34e20.
Plot twist, if your number is in a reasonable range this is very fast because there was one a popularJS benchmark that did this.