O(n^2), again, now in Windows Management Instrumentation
randomascii.wordpress.com
randomascii.wordpress.com
Things were working fine and performance was good. Then one day Windows Explorer suddenly hung with 100% CPU for couple seconds. This was one of the worst kind of bugs. There's no crash to pinpoint the problem. Things still work most of the times, just slowed down intermittently. Luckily I was able to catch a slowdown and deliberately crashed the process in time. The call trace stopped in the bubble sort function. I immediately kicked myself - it's the classic case of O(n^2) blowup. The cache entries had been scaled up to couple thousands items and the exponential O(n^2) blowup to tens of million of iterations was having a real impact. I switched to merge sort and performance was back to normal.
Edit: I picked merge sort because the worst case was O(n log n), unlike quick sort whose worst case was O(n^2). Once burnt, needed to be extra careful with edge cases.
This is why you sometimes see complexities that are e.g. O(1.3894732894^n) in wikipedia articles on the best known cases for various algorithms.
Typo: O(a^n) is a stronger guarantee than O(b^n) if a < b. It means nothing if a > b.
I actually thought the most likely intended meaning was that an algorithm in O(a^n) must take asymptotically longer than one in O(b^n) as n goes to infinity (if a > b), which isn't true. (For example, when a > b, then every algorithm in O(b^n) is also in O(a^n), but obviously no algorithm can asymptotically require more time than itself.)
"Exponential" is indeed misused.
Half the time I hear it, it is lower, like quadratic or cubic. The other half the time is higher, like combinatorial.
But even without any scaling factors, don't underestimate the size of the numbers involved. If you had two threads, one simply counting up to some n! upper bound, and the other up to 10^n - how long do you think the minimum problem size would take for the "slow" combinatorial to actually be slower than the exponentiation? Hint: at 4GHz and 1 op/cycle... Let's just say I'm not holding my breath that our species will still be around then. And even a tiny scaling factor to the mix...
I mean, if you're trying to estimate computational complexity at least, this nuance seems pointless.
But quadratic vs. exponential really matters, with plausible parameters.
I'm curious if the rise of quantum computers will make the differences between exponential and combinatorial meaningful in a practical sense.
There's certainly a most awesome sort algorithm which is exponential...
https://www.dangermouse.net/esoteric/bogobogosort.html
They are not even sure what the complexity is but it's like O(n!^(n-k)) or O(n*(n!)^n).
Of course, poly still means there is an exponential in the time complexity.
It's really nothing beyond the main takeaway that the usual norm to describe something as having "exponential" growth is that the number of computations increases in order of magnitude every time you add but a single item to the list of inputs.
- Quadratic = x^2, e.g. 0,1,4,9,16,25,36,49,64,81
- Exponential = n^x, for example 2^x, e.g. 1,2,4,8,16,32,64,128,256
Already very bad for small x even if n=2, but for n higher than 2 you can imagine you will run out of time very, very quickly ;-P
I'm not sure why, but getting back to the right terminology/vocabulary just to get back in to the basics of mathematics is a lot harder for me than it was 20 years ago.
If you seek to acquire formality and rigor, I can't help you there; I figure shortcuts will be nonexistant ;)
I had a slow week babysitting something else so I just let it chew away in the background, while doing other things like documentation and requirements work, noting down the time, adding one more module and running it again. The last one I was willing to do took 18 hours to load. It was still running when I got back to the office the following day.
To this day, I can't recall ever seeing any production performance bug with greater than n cubed complexity. This bug progressed at n to the fifth. Truly, a thing of singular beauty.
Thankfully they had a workaround that dropped it to something like n^2 (30 minutes became less than 5) and a bug fix not too long after.
You need to look up what exponential means.
> Dawson’s first law of computing: O(n^2) is the sweet spot of badly scaling algorithms: fast enough to make it into production, but slow enough to make things fall down once it gets there
But at the beginning, nobody was planning for that call to be fast - implicit requirements lead it that way.
In some ways, being able to identify and detect resource usage is what is nice about waterfall. Identification of critical API calls and respective timings would be integral to continue building the GUI elements. But we all know how waterfall is poo-pooed these days.
Taking things iteratively, small iterations, delaying decisions, learning as you go — that’s what agile development is. And all the stuff you mention above is possible when developing with agility.
If you've worked out which API calls are happening and how often, you've already written most of the application. It's just that it might be on paper or in pseudocode.
Waterfall was abandoned because getting to that level of detail takes far too long for management to accept and it's easier - sometimes orders of magnitude faster - to write the thing as actual code, try it, and then see what needs improving. And see what was wrong with the requirements.
Unfortunately there are cultural/psychological factors which often preclude or discourage this method, even if it would save time and produce better code in the medium term than either exhaustive planning up front uninformed by practice OR just iterating the initial broken version to continually meet new requirements.
My suggestion of building a first test version to, amongst other things, at least get our feets wet with the programming language (which most of us had hardly any experience with), then throw it completely away and restart "from scratch" (but with a better idea as of what we were supposed to do) was rejected.
Which has led to the ridiculous software bloat we have today. This always MVP, break fast, break often garbage needs to die.
Those are valuable things to build, IMO. Just make sure you leave out enough features that it cannot be pushed into production once someone else sees it running.
For application code it's probably better to express clearly what you're trying to do, in this case keyed lookup. Throwing in a linear search at that level isn't just dangerous, it's potentially confusing to people reading the code later.
Now, that doesn't mean you can't hash a, b, c, and d. It just means that the logic around doing the lookups is nontrivial.
When you are talking about < 100 elements, sometimes the consideration is "Welp, this is < 100, so lets just n^2 it".
Confused, what do you mean when you say the key is an "if" statement?
This comes up frequently when looking for fuzzy "duplicates".
For example, sometimes you get data from 2 sources, one will send it through with 0.01 precision. Another will send it through with 0.001 precision. You can't hash or compare that.
Things get more tricky when different finance institutes used different terms for the same entity.
That doesn't mean you can't avoid n^2 duplicate searches. It just means you have to be smart about how you group things.
[EDIT] it also makes you hesitate & second-guess and worry and experiment a bunch when contemplating using a recursive algorithm, which is deeply counterproductive in interviews where the expected behavior is so often "apply recursion, instantly and without hesitation" :-)
It’s like a postmodern religion where premature optimization is the cardinal sin, and you are supposed to burn as many cycles as possible to demonstrate that you are of good faith
Personally I would prefer to use different semantics to express that. It seems big O notation often carries a separate meaning in parlance.
I thought big O notation was about classifying function growth irrespective of the coefficient/constant. Does it not grow at all with respect to n? great you are O(1). Does it grow linearly? cool you are O(n). Logarithmically? Quadratically? In my mind, this kind of analysis aligns with big O notation.
Big O notation is for describing how the performance of an algorithm changes as the size of its input changes. If the size of the input is not a significant concern, then it's totally fine to not use big O notation for analyzing the problem.
The best kind of correct. Calling it O(n) then is the best way to express the performance improvement - I think that's the whole point of Big-O notation.
However I agree that for smaller n one should not underestimate the constant factor impact.
O(2n) is not an abuse of notation. It's just as well defined as O(n). The fact that O(2n) is a subset of O(n) is a theorem, not part of the definition of the notation.
In reality, O(n) and O(2n) can be quite different for small n. As can O(n)+k0 and O(2n)+k1. Or worse, O(n^2)+k2 where sufficiently large k0 and k1 make the quadratic system better because it's k2 constant is so much smaller. Setup time matters.
Nowadays, you rarely have enough elements that the asymptotic behavior is the defining performance characteristic.
Then my brain asked why you'd want to reduce a quadratic algorithm to exponential.
https://accidentallyquadratic.tumblr.com was / is pretty much an entire blog dedicated to this.
for i in n:
for j in n:
whoops(i, j)See this stack overflow on polynomial vs. exponential: https://stackoverflow.com/questions/4317414/polynomial-time-...
whoops(n):
for i in n:
whoops(n-1)
Your version is merely quadratic.that is not exponential, that is actually factorial, which is superexponential. Uh, or close to factorial, I'm not sure exactly. Testing it in python right now, incrementing a counter each time whoops is called, I'm seeing a relationship that looks like whoops(n) == n * whoops(n-1) + 1 That is interesting.
Fair point; I should have given:
whoops(n):
if(!n) return
for i in 2: whoops(n-1)
I think it's clear that it is "trivial to go exponential", though. And for that matter, also trivial to go superexponential apparently.I recently illustrated how they can even occur as a result of algorithms being slow by a constant factor.
I thought it might be useful for others, so I posted it here: https://news.ycombinator.com/item?id=21745911
One day I got a call from the customer that his analysis was taking 6 hours to run, for a program that should have finished in a fraction of a second. It turned out the customer had tried to load a 30,000 row spreadsheet of input. The program would on every loop iterate over the input association list, resulting in classic O(n^2) performance overall.
After changing all the places to use a hash table it again ran in sub-second, although the code was nowhere near as elegant.
Because I'd expect all maps to provide roughly similar interfaces whether they're assoc lists, hashmaps, btrees, HAMT, …: iterate all entries, presence of a key, get value for key, insert (key, value), remove key (and value), possibly some other niceties on top (e.g. update value in-place, merge maps, …) but those are extras.
let m' = (k, v) : m
to let m' = insert k v m
What there would make the code "nowhere near as elegant"?In contrast, languages like Perl or Python have literals and syntax support for both lists and mapping-type data structures (hashes in Perl, dicts in Python), so using either one is roughly equally elegant, and you are more free to choose one or the other based on performance concerns.
But well. In Haskell assotiation lists are also the default, as they don't impose any stictness and are very fast to iterate. So that is what you get from libraries.
If you need a map, you will have to first construct it, then run your code. But of course, if you are the one creating the lists, it does change very little.
To make an n^2 algorithm n (or n log n), you pretty much always need to add in some constant time or logarithmic data structure. That requires tracking that structure, usually generating a good key, etc.
I'm not saying that's really all that extreme. It just means your previous 5 line algorithm will often "bloat" to 10 or more lines with a new concept to track. This is where I see some saying "not elegant".
> Being written in a functional language meant that using association lists (linked lists for key/value) was very natural. […] After changing all the places to use a hash table it again ran in sub-second, although the code was nowhere near as elegant.
You're not tracking more things (just tracking a hashmap instead of a list), you're not generating anything different, you don't have any new concept to track.
All I'm asking is where the apparently significant loss of elegance would come from.
In a word, you're losing a persistent data structure[1]. And this can go beyond just loss of elegance. Sometimes, functional programs will depend on immutability of data structures for performance optimizations and even for functionality (like keeping a version history).
It's true that there are a few 3rd party implementations that I've noticed before, but not sure how robust and well-tested these are. I'd hesitate to use them or something I wrote in production without careful review.
In any case, persistent HAMTs are a relatively new phenomenon that are just starting to catch on, and this story is described as being "back in the day". It's unlikely this option was available to him even if he were willing to write an implementation himself or use an untested 3rd-party implementation.
Maybe one will be added eventually to one of the OCaml standard libraries, which would be great.
A trivial example, I recall about 1 month ago calling it out on a Javascript code review where someone was doing a `someList.find(x => x === "something")` inside a loop creating O(n^2) complexity. Rather than change the entire codebase wherever `someList` is used into a new type it is often easier just to suggest we build a Set (linear on length of `someList`) before the loop and use that for lookups within the loop (constant).
Of course, I am talking about circumstances clearly outside of your original model of OPs description. I am tracking more things (the new Set). However, I realize that to give the full context as to why the type of `someList` from my trivial example _couldn't_ be changed easily and why creating a new Set was the most prudent option would require a comment even longer than this essay. So I give OP the benefit of the doubt that he was in a similar situation where using the hashmap everywhere was either difficult or impossible.
let max_map_regexp = 500 (* lines - see below *)
(* Format of the map_file. *)
type map_file_t =
| Map of ((Pcre.regexp * Pcre.regexp *
Pcre.regexp * Pcre.regexp) *
(float option * bool * Inp.params)) list
| Hash of (string * string * string * string,
float option * bool * Inp.params) Hashtbl.t
let run map_filename =
(* Parse the map file. *)
let csv = Csv.load map_filename in
if Csv.lines csv < 2 then
failwith (map_filename ^
": bid file should contain headings and at least one row of data");
let csv' = List.tl csv in (* Ignore headings. *)
... (* If the map file is > max_map_regexp lines long then regular
* expressions are banned and a more efficient hash table format
* is used for lookups. Otherwise the program takes far too long
* to run.
*)
let map_file =
if List.length map_file <= max_map_regexp then (
Map (List.map (
fun ((c_name, ag_name, kw_text, kw_type), a) ->
(compile c_name, compile ag_name, compile kw_text,
compile kw_type), a
) map_file)
) else (
(* Regexps banned from large map files. *)
Hash (
let h = Hashtbl.create (List.length map_file) in
List.iter (
fun ((c_name, ag_name, kw_text, kw_type), a) ->
Hashtbl.add h (c_name, ag_name, kw_text,
String.lowercase kw_type) a
) map_file;
h
)
) inOr maybe I just didn't know the tools well enough?
Vista was a massive release, but also much maligned so maybe you didn't miss much leaving when you did. The tooling has certainly gotten a lot better since then, and so has Windows.
Rather more recently, Windows has gained dtrace support: https://techcommunity.microsoft.com/t5/Windows-Kernel-Intern...
[1] - See this self-described eulogy by @SwiftOnSecurity: https://twitter.com/SwiftOnSecurity/status/85185740489147187...
I recently used Win10 for work and was amazed at the combination of awe-inspiring tech and plain awe-full complexity. Just the control panel goes four layers deep attempting to make things easier but in practice is several times harder to find options than it was in Win2k.
If there were a distribution with all the corporate goals stripped out I'd jump on it.
It turns out it was now taking about 1-2 hours daily and 6-12 hours on the weekend depending on data size. This had been going on for months but gradually getting worse as the data grew, to the point it was unbearable so finally reported.
A senior programmer had removed the shell call to sort on an indexed text file and written their own ad-hoc sorter through every fresh programmer's favourite (you guessed it) bubble sort. To make things worse, this was perl which has a perfectly functional sort itself if you really have to do it that way. I still have no idea why this was done, I don't think asking would have been productive in that place at that time.
Future OS/Compiler Programming Note: It would be nice if threads could be individually named and those thread names shown/slowed/stopped/debugged in whatever tool implements Task Manager like functionality...
[0]: https://docs.microsoft.com/en-us/windows/win32/api/processth...
Windows had a janky way of naming threads before 10, but it has a supported API now.
The problem is that svchost is a host that runs a thread pool with various services being invoked as needed. You'd have to rename each thread every time you ran a function for a service.
That's probably more doable now with the supported API. The previous way involved raising an exception, so I can see why it wasn't done.
Seems like Windows has a more systemic testing problem though. We used to stress-test the crap out of various NT components back in the day. Don't know if the elimination of SDETs means there's no one doing that other than the NT perf team, which never did targeted stress.
This was well known, and many applications actually used that. Of course it was very ugly. I don't think it would have been a problem to also rename a given thread multiple times, but not sure.
That limit makes it pretty hard to provide meaningful identifiers in a non-trivial application.
Harmless I think, but sloppy.
I just kinda like trolling Microsoft for not using their own thread naming API very much.
https://randomascii.wordpress.com/2015/10/26/thread-naming-i...
Makes it into production but falls down eventually!
But that should be two queries. One for all the parents, and one to get the children with a sub-select (or at worst a manually generated parent id list), and then you can join them manually (if actually needed and you can't just use the data sets as they are).
what happens, is that API users start executing that endpoint N times and here it becomes O(n^2).
people should remember that APIs are for CRUD calls. If you want batch reports - call a separate reporting endpoints that process data in large batches.
You can try using IN on the second query, but usually if that was going to work in a reasonable amount of time, your join would have also worked in a reasonable amount of time.
The real problem people run into with the client side join is making it query one get a bunch of ids from table A, query two through N, get one row from B, with each query requiring a round trip. Even a pretty small client to server roundtrip of 1 ms gets nasty quick with repeated queries.
While it can add some perceivable latency if you have many levels of depth, it is usually a lot lighter on CPU and memory than one big query.
The reason I really like it is because it is very easy to strongly type results coming from a single table, and processing the data in your application code allows you to keep the typings through the process of stitching everything back together.
Previous company had to do something like that because of an Oracle perf bug way back (outer join issue I believe?), but they fixed it eventually and it was all deleted.
Depends on whether your database is reasonable or not.
Sometimes "foo IN (1,2,3,4,5)" will do five index lookups, while a JOIN will check every single "foo" in the table.
For bonus points it will also convert "IN ([independent subquery])" into the same JOIN. So just by reminding the database that a thing inside parentheses gets evaluated first, you can make it go a thousand times faster. Not a single other change to the query.
Anyway, it was running 29000 database queries as it was repeatedly using an "in" clause of 1 item across hundreds of queries, instead of 1 query with hundreds of items in the "in" clause.
I replaced them with 1 query that fetched all it needed instead of the 29000 queries (and 8000+ which were duplicates).
Terrible code.
If you write a tower of 2^2^9 on a whiteboard and ask 1000 mathematicians and computer scientists to evaluate it, I'm sure 999 or 1000 of them would evaluate it as 2^(2^9).
[0] https://en.wikipedia.org/wiki/Order_of_operations#Serial_exp...
In this case, the absurdity of the number suggests a more realistic number.
What is kinda OK today is going to definitely not be OK going forward. This algorithm has to get fixed.
One other aspect of the issue is that it's unclear why that database is now so large. It seemed like different machines had different sized databases — perhaps one angle of the solution is trimming and vacuuming.
I agree that finding out why the repository is huge seems worthwhile. I think it's been growing lately which means it might eventually get to an unsustainable size. As far as I can tell Microsoft doesn't ship any tools to make repo-size analysis easy. An open-source tool was suggested in one of the comments on my blog.
Exactly. It's 1.9 GB on your machine, whereas on a plain home-use computer it's less than 50 MB, i.e 40 times smaller. Something produces all that data there, what is that, and what's that that's being stored?
Instead the repo was being verified as a side effect of another WMI operation.
I think that the hourly verification was put in to try to diagnose some problems that were actually or suspected to be related to WMI corruption.
Even daily verification is going to cause problems, they are just less likely to be noticed, until they get to the 30+ minutes level.
The reason I’m hating on developers that use WMI when alternatives are available is because WMI is dog slow and everyone that does low-level Windows development knows it.
And they tried blaming them for migrating data to a new server with an SSD that shaved 20 minutes from their processing time.
And if you're wondering, they refused to fix it because "it would need too many sprints" and "maybe we'll talk about it in a workshop".
It's still not fixed.
I had to inject javascript into some vendor code to avoid this after our production environment died. I ended up replacing the underlying hash-map into multiple smaller maps based on a hash of the items. So I'd have 50 maps of 1000 items instead of one map of 50000 items.
It's a medium-sized inventory, if you need fast / offline access then loading it to the client makes a lot of sense. I'm sure there are plenty other things of which you can easily reach 50k, and that you'd want to index or cross-reference somehow.
* Editing objects in a 3D scene(e.g. a AAA open-world game or a CGI film)
* Plotting events in a long-running log on a graph
* Scraping and presenting data from web sources
The common thread here is that you have most of your data in application memory but not necessarily in a formal database system, and so you shoulder the full burden of managing it properly. In most cases the solution is to define a real backend and filter it there, paginate or otherwise reduce the amount that gets presented because data at that scale won't be usable by humans to begin with. But sometimes you do have a reason to explicitly want a "big list of everything," and equally as often you end up with the big list of everything just by accident. It just comes with the territory of report generation tasks.
I learned that just replacing the whole page's innerHTML by a concatinated string on each frame/mousewheel event is way faster then updating the cells content and position individually, and resuls in butter smooth scrolling.
It was a data visualization tool which let users make dashboards based on your data-warehouse. You could add filters to let users "slice-and-dice" the data in the dashboard.
Needless to say one of the measures had a large number of parameters. The dashboard in question allowed users to see the change in measurements of [X] over time where [X] was one of 50k poisons that the Environment Agency were measuring.
It a pretty reasonable question and - in browsers with O(n) hash lookup in their hashmaps - performed excellently.
We also regularly had grid containing millions of items (or snapshots thereof).
https://stackoverflow.com/questions/12808934/what-is-p99-lat...
One of the apps that used to be maddeningly slow was Instagram, but Instagram has gotten better lately. No idea why though.
So for example, I almost always use associative arrays (maps) instead of lists. I actually really wish there a MAPP language because I view the map as a potentially better abstraction than the list in LISP, but I digress.
I also tend to use atomic operations instead of locks. Some good starting points for that are understanding how compare-and-swap (CAS) works, and also how functional programming with higher order functions and immutable variables works because that mindset greatly simplifies threading with no shared mutable state for the Actor model. Lazy evaluation is another good one. Also vector languages like Gnu Octave and MATLAB are good because they favor a level of abstraction above the bare-hands programming of C-style languages like C++ and Javascript so you tend to see that most algorithms are embarrassingly parallel at some level (especially the things we tend to think of as computationally expensive like multimedia processing).
Also (this may be controversial) but I think that poor performance can be politically motivated. For example, I run Safari with Javascript disabled so I can have thousands of tabs open (it's disabled as I write this). But when I disable Javascript in Chrome, performance grinds to a halt. You can try it right now on the Mac by force quitting Chrome with a bunch of tabs open and relaunching it from Terminal.app with:
open -a "Google Chrome" --args --disable-javascript
Or manually with:https://www.computerhope.com/issues/ch000891.htm
I don't know what causes it, but my guess is that Google either wrote some of the loops under the assumption that Javascript would always be on, or they had a blind spot in their implementation because so much of their business model depends on ads having dynamic behavior.
So when you're in a meeting and someone shouts down your concern about edge case performance because they don't see that as a priority, graciously humor them and then write your code the right way because you know that it doesn't take any longer than doing it the wrong way. You might catch some flack during code review so have a good excuse handy, something about trying it the other way but running into problems. Often I'll write the easy imperative solution in a comment above the simple functional solution or even put both solutions under a preprocessor directive or feature flag to leave it up to the team lead/project manager and have our keisters covered if/when something melts down.
To see the difference.
- 100^2 -> 10^4, 2^100 -> 10^10
- 1000^2 -> 10^6, 2^1000 -> 10^100
Look at the difference in order of growth. Just to give a very simplified comparison, your individual processor core can run about 10^9 simple instructions in a second.I recall that some people would put GOTO statements to jump across large comment blocks in the days where reading a few KB was noticeable.
http://xset.tripod.com/tip3.htm "COMMAND.COM reads and executes batch files one line at a time; that means that it reads one line, execute it and rereads the file from the beginning to the next line." Perhaps this is a really old version of COMMAND.COM? Perhaps it's poorly stated but actually meant that it "rereads the file from the beginning of the next line to the next line".
https://docs.microsoft.com/en-us/windows/win32/fileio/local-... "Command processors read and execute a batch file one line at a time. For each line, the command processor opens the file, searches to the beginning of the line, reads as much as it needs, closes the file, then executes the line." This also seems to agree with my original claim.
I tested this with a batch script generated by
print("@echo off")
for i in range(1000):
for j in range(1000):
print("rem hello world")
print(f"echo {i}")
and ran it using COMMAND.COM in a Windows 95's VM. It appears to run in linear time. Either COMMAND.COM was fixed, or both sources are incorrect.And you can certainly write self-modifying batch files, but only appending at the end is safe, not changing lines before you're currently executing.
That's what Windows does now. But I think in the past it wasn't that way (as suggested by the sources). In my tests it appears that the command processor caches the byte offsets of each line, so it's possible to GOTO any line in the past without rescanning the whole file.