Hacker News new | past | comments | ask | show | jobs | submit login

Do you want to claim you've never written quick and ugly code to get something working to come back and fix it up later?

Pretty much everyone I know will throw down an O(n^2) algorithm or whatever in their first pass and replace it with something more thought out once they have the time to think deeply about it.

If you're fretting about optimization at every stage of development, you're really doing it wrong. This is precisely the type of early optimization that everyone and their dog will tell you is bad. You should focus first on making your program work and laying out the logic and data flows. Optimization does not matter until the program is almost finished.




> Pretty much everyone I know will throw down an O(n^2) algorithm or whatever in their first pass and replace it with something more thought out once they have the time to think deeply about it.

Most of the times I've seen this, the faster algorithm is literally just a dictionary with a well-defined key.

I honestly do not understand why that's not the first solution most devs think of and why n^2 seems to dominate.

As an example, I've seen code like this quiet a bit

    result = []
    for (item: items) {
        for (item2: items) {
        if (item != item2 && item.foo == item2.foo) {
          result.add(item)
        }
      }
    }
Easily replaced and massively easier to read as

    result = []
    itemFoo = HashSet()
    
    for (item: items) {
      if (itemFoo.contains(item.foo))
        result.add(item)
      else
        itemFoo.add(item.foo)
    }
The mindset of re-looking for values in a collection you are already iterating through is just foreign to me. The first solution for something like this in my mind is always utilizing dictionaries.

The first version is more readable, faster for small n (which is the common case), can not run out of memory due to the hash table, compiles to less code, does not need item to be hashable and cannot accidentally copy elements (depending on language). It _should_ be your default. (Well, except you should start from the items after item1, so that you don't need the item1 != item2 test.)

> The first version is more readable

Disagree

> faster for small n (which is the common case)

How can you know that? And for smaller n, the choice hardly matters.

> can not run out of memory due to the hash table

If n gets large enough that hash table size becomes a problem, n is large enough that you need the hash table or some other structure that reduces the complexity down from O(n^2).

> compiles to less code

This is just nonsense. Any codebase of significant size already has a hashtable setup and unless you are using a language like C++, the code size of using a hash table anywhere is constant. The cost is already paid.

The actual amount of compiled code from the switch is identical.

> does not need item to be hashable

True.

There would be reasons not to choose option 2, but it should be default until it's been proven to be a problem. And even then, the next step is almost certainly not going to be option 1.

But then this could also be language and application specific. I deal primarily with backend services/Java where memory is plentiful, and allocation is fast. Algorithmic complexity is for my applications very often the better indicator of what good code should be.


For many people performance optimization really starts with your second version. I'm not even sure what this is supposed to do exactly but the O(N^2) is just bad/sloppy code. Optimization is usually more about getting the right algorithms to execute faster. At least that's how I think about it.

Yet, as you'll see in this thread at least 1 other commentor is arguing that option 1 is better. What you and I see as bad/sloppy code, they see as the ideal starting point.

A pretty common attitude that I've ran into. People take the "premature optimization" quote to mean "Never think about big oh when writing code".


Beware of microbenchmarks, but at least on my machine under the specific circumstances I ran it under, the double loop algorithm is about two times faster than the hash map algorithm while n < 100. That might not mean much if you spend your days working with large datasets, but in my day-to-day datasets with 100 entires or less is the norm. Readability is subjective, but at least from a performance perspective it is a reasonable starting point unless you know upfront that you are going to be working with large datasets – but in that case the optimization isn't premature, is it?

Here's the main issue I have with this reasoning. It builds in a fault to the program that doesn't need to be there.

If n is usually less than 100 then sure maybe it does make sense to do the more brute force approach. However, what's the impact of using the hash table first? If n is usually less than 100, now this method is slower usually, but is that a problem? Well, you should use a profiler to find out if it is or isn't. However, that may be entirely tolerable performance wise.

On the other hand, if your assumption that "n < 100" doesn't hold up and, indeed you start seeing n > 100, or 1000, or 10000. Now all the sudden this method becomes a fault in the application you developed that needs to be rewritten.

What's devious about the "n > 100" problem is when you first write the code, you may have very well been correct that "n < 100". But time and inputs change the way code is executed.

A lot of the code I've fixed has been due to this. I've seen the counter where really searching a list/array was the better option only a handful of times.

To me, premature optimization is using a known algorithmically inferior approach, of the same complexity as the superior approach, because you know that for small n, it is faster without knowing whether or not that will be the case.


> It builds in a fault to the program that doesn't need to be there.

Not really. You're just exploring an unknown frontier. Once the frontier is known and you actually understand what you are trying to solve you're still going to put in the right algorithm.

I mean, you might not if you're vibe coding, which I guess is apparently a thing somehow, but I'm assuming we're talking about actual engineering here, not speaking the sordid tales of cowboys. The "premature optimization" line was definitely not directed at those who think code is a feeling.

When the frontier is unknown, whatever algorithm gets something operating as soon as possible is the right one. The given example may be so contrived that the hash map would be what you see as the quickest and dirtiest solution anyway, but with a little more complexity loops do tend to be easier to reason about on a first-pass basis.

> Now all the sudden this method becomes a fault in the application you developed that needs to be rewritten.

In the same way that finding out that a blue button doesn't resonate with users, requiring you to rewrite your application to make the button green. Such is the life of building software. But that's why engineers get paid the big bucks – nobody else is stupid enough to do it.

> A lot of the code I've fixed has been due to this.

You're not really fixing it, you're updating it to reflect the information you gleaned during the exploratory phase. I mean, unless you're coding on vibes or something, but, again, assuming actual engineering is taking place here. Nobody should be coding non-prototypes by the guide of their feelings.


We're not really benchmarking here. The author of the code does need to understand the size of the data they're operating on etc. I also didn't critique the hashmap version. Using hashmaps [for] everything is also "lazy".

Good code should follow something along these lines:

- A reasonable understanding of what needs to happen. What are the requirements. Including data set sizes. Including how "hot" this code path is.

- A reasonable pick of the algorithm to perform what needs to happen.

Sure. O(N^2) for very small data sets could outperform an O(NlogN) or O(N). The conflation of big oh notation with actual performance is just another form of sloppiness as is picking a bad algorithm. In both cases it reflects a lack of understanding of how computers work and how to do things efficiently.

Let's look at what we're trying to do here. We want to find all values that occur more than once in a vector. Using a hash map means you have to:

- Create this data structure on the fly.

- Hash the values.

- Do the insertions/lookup.

You should consider the size of your dataset, you should consider locality/caches etc.

100 entries squared is 10k though. If this is part of a critical code path, let's say your frontend code does this for every DOM element or every notification it needs to process this can end up the bulk of CPU usage. This sort of stuff is why I've seen applications cause the fans to go on or the browser to crash. I would say bad O(N^2) algorithms for large Ns are pretty common.

I would be surprised if e.g. a C++ implementation of the hash version doesn't win over the double loop for data sets of 100. But maybe that's borderline. For a dataset of 1000 there's no way the double loop is winning.

Other alternatives might be something like an efficient small bloom filter that can be stored in a register, sorting the array. I mean at least cut the iterations in half by not iterating over the entire range in the inner loop!

Then you won't have the trouble with:

  if (item != item2 && item.foo == item2.foo) {
being a bit less readable then you might think.

> I mean at least cut the iterations in half by not iterating over the entire range in the inner loop!

The reason I wrote it that way is because the actual common way I find this is in Java where the team wants to use the `stream` api.

IE

    items.stream()
      .filter((item)->items.stream()
        .filter((item2)->item2 != item)
        .findAny((item2)->Object.equals(item.foo, item2.foo)).isPresent())
      .toList();
That's not really super readable or universalizable to other languages.

Even more commonly, instead of doing the `findAny` I often see the trip up being someone doing a `contains` or other O(n) operations on `List` objects inside iterations of that list.

It's this sort of fluent/functional style of programming that often seems to get tripped up on doing n^2 (or more) algorithms.


This only works if you don't need real time performance. For anything that does need to run in realtime, you have to be damn sure that whatever your initial approach is can be optimized to run at the required sample rate. Otherwise, you will be ripping out the entire design and starting over. You might as well optimize first.

Purposefully introducing underperforming code as a tradeoff to meet some other goal (faster delivery, for example) is something else entirely. It doesn't require intuition or profiling – at most requiring only memory of the choice. A fun aside, but not related to what we're talking about.

One could even say optimization does not matter until the program is already running.



Consider applying for YC's Summer 2025 batch! Applications are open till May 13

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact