When Big O Fools You - <AUTHOR> https://jackmott.github.io/programming/2016/08/20/when-bigo-foolsya.html <comment><USER3> This article points out a few right things, but also skips over the wrong parts.

Namely, the amortised time complexity of dynamic lists.

Amortised analysis treats operations not as single events but looks at the time complexity over the span of many operations (through something called "Accounting"). An initial "investment" of array over-allocation will be amortised by inserting, but only over time. Inserting into an ArrayList will be in O(1) given enough insert operations.

Essentially, you over-allocate in order to save time. You're trading time complexity for space complexity.

A really good explanation on amortised analysis can be found here on Wikipedia, which explicitly treats ArrayList:

[https://en.wikipedia.org/wiki/Amortized_analysis#Dynamic_Arr...](https://en.wikipedia.org/wiki/Amortized_analysis#Dynamic_Array)

As always, a look into the source code of your language of choice helps. In Python, a list object over-allocates using a very peculiar, but finely tuned formula: [https://github.com/python/cpython/blob/09dc3ec1713c677f71ba7...](https://github.com/python/cpython/blob/09dc3ec1713c677f71ba7c536140ce12801a5036/Objects/listobject.c#L42)

<comment><USER4> The nuance you're missing here is that the author is always inserting at the front of the arraylist. So even though the list grows dynamically, it still needs to move every value one spot over.

e.g. arraylist: [0][1][2][3][ ]

Even though there is space left in the array, we still need to move all the values one index to the right to be able to insert at the front, which takes O(n) time. In contrast, inserting at the front of a linked list is simply a matter of moving pointers and therefore constant time.

<comment><USER5> Can't you just imagine it is reversed, where the_array[num_things-1] is the first ?

<comment><USER6> Yes, then it would be a discussion about append performance instead of insert performance. Might be handy to do if you know that all the changes to an array will be inserts at the front, then you can just write the array in reverse and also read in reverse.

But it is helpful to keep the terminology clear, because an insert in the middle of a linked list is still O(1), but inserting into the middle of an array will require moving some fraction of the data.

The OP is discussing inserts at the start just because it is the worst-case scenario for arrays, then a positive result will still be true if some of the inserts are in the midddle or even near the end.

<comment><USER7> > But it is helpful to keep the terminology clear, because an insert in the > middle of a linked list is still O(1), but inserting into the middle of an > array will require moving some fraction of the data.

This is true if you already have a reference to the middle of the list. If you don't (say, because you want to insert while preserving the fact that the list is sorted), then inserting into a linked list is O(n) just like the array is.

<comment><USER8> Memory reads and memory writes are not the same. Linked list insert in the middle needs to do only O(1) writes. But because linked lists preclude memory prefetch, they should not be used these days.

</comment></comment></comment></comment></comment></comment><comment><USER9> In no so many words, the article is pointing out that O(n) + O(1) is not necessarily quicker than O(n) + O(n): both add to O(n) (since only the highest-order factor matters), and the constant factor is unsurprisingly better for array lists.

He's not benchmarking an O(1) operation against an O(n), or anything surprising, or even pointing out a "hidden" O(n) operation, it's simply a demonstration that O(n) + O(1) = O(n).

<comment><USER10> Of course this was a single example.

The main point is that the big O notation ignores the cache misses.

For example:

\- try writing a quicksort as a template to avoid the comparison function calls

\- in the sorting loop, switch to an insertion sort when buckets have <= 32 elements

You will see speed improvements of 2 to 3 times faster than a vanilla implementation of quicksort.

Although this approach is slightly more complex on the abstract level, it's faster because it reduces lots cache misses.

<comment><USER11> Surely the _main_ point is that

        O(n) + O(n) = O(2n) = O(n) = O(n + 1) = O(n) + O(1)

and the constant factors left out of O() often dominate execution time.

Cache performance is just one example of a constant factor, right?

<comment><USER10> Cache performance is not constant, if it was, we wouldn't even consider it when optimizing.

The point is to use data that was recently fetched into the (faster) cache memory as much as possible instead of incuring the penalty of a cache miss

Cache performance really depends on memory access patterns.

Anyways :) I'm being pedantic here, I should probably go back to work.

<comment><USER12> Memory access time is a constant multiplier, whether it hits or misses the cache. There can be a big difference in that constant depending on the access pattern, but it is still considered a constant factor.

Yes, random access (missing the cache every time) may be 100x slower than sequential (hitting the cache almost always), but if you're iterating through an array twice as large, it will still be 100x slower.

<comment><USER10> I agree, on the same machine, the slow-downs are constant for each level of cache and for ram access.

</comment></comment></comment></comment></comment></comment><comment><USER13> A cache miss can be a slowdown of ~200x on modern CPU architectures. Lets say you have a choice of a O(n) algorithm that always misses the cache and a O(n lg n) algorithm that never misses the cache.

The crossover point where it makes sense to use the O(n) algorithm will occur when:

200 * n = n lg n

which occurs for n > 2^200. Most of us are not dealing with problems larger than can be represented in our universe, so cache efficiency matters, kids.

Note that this changes for other comparisons. Going from n maximally cache inefficient vs n^2 cache efficient is only worth it for n up to 200, and n lg n --> n^2 up to n about 2223.

Of course, in practice your algorithm won't be missing the cache on every access, so reality is somewhere between these values.

</comment><comment><USER14> _Make Sure the Abstraction is Worth It_

Yes! All abstraction has a price. This is what people really mean when "all abstractions leak." Basically, all code and runtime features have a cost in terms of developer resources, cpu, memory, etc. The "inner game" of development isn't being able to muster huge amounts of "cleverness" power. The "inner game" is being able to put whatever resources you have to the best possible use.

<comment><USER15> This isn't true at all. What's the cost in developer resources, cpu, _or_ memory of making a complex number class in C++ a template over floats and doubles? How about the cost of using Rust's generics to make a parser that can parse from any linear source of bytes (e.g. both files and in memory byte arrays)?

There's plenty of abstractions which don't actually cost anything, and there's room for even more.

<comment><USER16> What's the cost in making a template more generic? Greater compile time. Need for compiler that can handle generics. Greater number of functions in binaries, and thus more unique places for bugs to occur. Dynamic library bloat. Harder to understand code. Quirky edge cases. Harder to learn languages. Harder to _parse_ languages. And of course, developer time is spent dealing with all of this.

Abstractions leak because they are _models_ , and thus are necessarily different from the concrete things that they model. These differences can and will cause problems. Generic programming is an abstraction that pretends you aren't working on low-level un-typed system. And you pay for that abstraction any time you need to deal with low-level demands through that abstraction.

<comment><USER15> > Greater compile time.

Not really with newer generics implementations (.NET, Rust, etc.). I don't think anyone thinks C++ templates are ideal, and the others show this at least a non-essential cost.

> Greater number of functions in binaries, and thus more unique places for > bugs to occur.

Less human written code, so this reduces the surface area to the compiler; which is an "issue" no matter how low level your code is.

> Dynamic library bloat.

Your alternatives are A. duplicate the classes yourself B. let the compiler duplicate them. Where's the bloat? C++ _requires_ you to explicitly instantiate templates if you want them in a library, and usually they're just left to the header. The others I listed won't generate anything unless you actually use a particular instantiation.

> developer time is spent dealing with all of this

It's pretty hard to argue that more developer time is wasted by the compiler writer than that of the time saved for developers who use the language for such a broadly used feature.

> Abstractions leak because they are models, and thus are necessarily > different from the concrete things that they model.

What concrete thing do generics model? Tediously copy-pasted code? These will only be different if you mess up when you do the copy-pasting.

> Generic programming is an abstraction that pretends you aren't working on > low-level un-typed system.

What? It's an abstraction over an _already_ typed system that obviates the need to duplicate code, which after the generics are unwound produces the same code you would have had anyway. You're just as far or close to the low-level un-typed system as you were before.

"All abstractions leak" is a nice platitude, but handwaving about "models" and "concreteness" doesn't prove anything.

<comment><USER16> You seem to be misunderstanding the generality at which I'm speaking. Take this:

>It's pretty hard to argue that more developer time is wasted

That's NOT what I'm arguing. I'm saying that it takes a _different_ amount of time. Hence the concept of "trade-offs", AKA "cost."

With that said...

>Your alternatives are A. duplicate the classes yourself B. let the compiler duplicate them. Where's the bloat?

C. Monomorphise at runtime.

>Less human written code, so this reduces the surface area to the compiler; which is an "issue" no matter how low level your code is.

Yes, but it's a different issue. Again, there are trade-offs.

>What concrete thing do generics model? Tediously copy-pasted code?

No. They model the behavior of the algorithm as it physically exists in a machine.

>What? It's an abstraction over an already typed system that obviates the need to duplicate code, which after the generics are unwound produces the same code you would have had anyway. You're just as far or close to the low-level un- typed system as you were before.

Generics are part of your language. They do not abstract over the language. They abstract over the behavior of your program. It's just a feature of the language that does so with greater generality that the rest of the language.

And the code you "would have written anyway" is _machine code_. Which you _are_ abstracting over. Generics allow you to pretend your algorithm doesn't need a fixed machine representation. The fact that you can produce two different machine representations for the same template code is an example of what I'm talking about, not a counter example. The abstraction leaks. You write one algorithm, and it has to be compiled into two different representations. That's the cost of the abstraction. You get increased generality at the cost of memory and time.

<comment><USER15> > Generics are part of your language. They do not abstract over the language.

Most generics systems allow the code to be unfolded into generic free code; this is how templates are usually compiled in C++. So in a material sense the compiler treats templates as syntactic sugar added over C++ without templates (which is a valid language, which the compiler uses internally).

> C. Monomorphise at runtime.

A lot of generics (like my complex number example) can't be monomorphized at runtime since floats and doubles don't have dynamic dispatch in these languages. Not to mention the loss of type safety even if you implemented it this way. So that is not equivalent.

> And the code you "would have written anyway" is machine code.

That's not analyzing the cost of generics, that's analyzing the cost of the whole language, and then putting it on generics. If that's fair, why wouldn't we add the cost of designing and operating computers instead of doing it on paper? There's nothing special about machine code here.

> The abstraction leaks. You write one algorithm, and it has to be compiled > into two different representations. That's the cost of the abstraction.

If (as in my example before) you need complex numbers with floating point numbers, and complex numbers with doubles, the compilation will likely be _faster_ because the compiler doesn't need to take two parallel implementations through the whole parsing process, and can instead generate the necessary ASG itself. If you use on only one, then maybe there is a detectable differential cost.

Also, how is that even a leak? That's the abstraction working absolutely perfectly and giving you exactly what you intended.

</comment></comment></comment></comment></comment></comment><comment><USER17> I agree with the fundamental point in the article, but isn't it well known that when you consider multiple levels of the memory hierarchy as a whole, big O notation needs to be modified to take into account the relative cost of access?

I think the problem is not so much the big-O notation, but that the underlying assumption that the data access is from an "all main memory" model with no cache or secondary storage is often forgotten.

For example, in database algorithms, all memory operations are often considered to be unit cost since they are an order cheaper when compared to accessing the disk storage?

<comment><USER18> Big O notation is typically counting comparisons, and ignores constant factors. It isn't that memory hierarchy is ignored so much as the equating of comparison growth (in n) with performance.

The field of cache oblivious algorithms is focused explicitly on memory hierarchy, and accounts for cache misses.

<comment><USER19> It's typically not counting comparisons, there are none when inserting elements. But sometimes it pretends that comparisons are O(1).

</comment></comment></comment><comment><USER20> Big O isn't fooling you, it's giving you a one-function explanation of how the algorithm runs on an idealized system as the size of the problem increases. There's other systems you can simulate for big O, but the math is much harder and you wouldn't generally use it unless you're doing something complex like a cache-oblivious algorithm.

If you're using solely big O to decide on an algorithm, you're fooling yourself. If performance is an issue, profile, benchmark, study, don't guess.

</comment><comment><USER21> Big O notation is asymptotic. Using only 5 insertions to understand Big O is definitely the wrong way. For example, insertion sort works better when the array is small, but insertion sort is definitely O(n^2), worse than qsort.

Don't let Big O notation fool you, don't misunderstand Big O.

<comment><USER22> Inserts in that chart is the ratio of inserts to reads.

5 means "write five times then read once per iteration" not "write five times".

</comment></comment><comment><USER23> _The result is that when you iterate over contiguous memory, you can access it about as fast as the CPU can operate, because you will be streaming chunks of data into the L1 cache._

This is not true at all. You'll be able to access it about as fast as the _memory_ can operate - it'll still be much faster than randomly chasing pointers all over the place - but the maximum processing speed of the CPU is an order of magnitude greater than that again.

</comment><comment><USER24> A lot of the focus in the HN comments is on ArrayLists, but I'm curious why the author chose to give linked lists an O(1) insert time. In some implementations (doubly linked list, inserting at the head of the list) I can see the O(1) time, but when appending to the end it also is O(n) because one has to traverse the entire list of elements before updating the link at the end of the list. Might just be nitpicking here but that might also be affecting the author's results.

<comment><USER25> Just keep a pointer to the head and tail of the list. You can then append to either end of the list in O(1). When you put a new node at the tail end, use the tail pointer to get to the tail node instead of walking the whole list. Of course, you need to make sure the head/tail pointers are always updated to point to the current head/tail of the list.

</comment></comment><comment><USER26> The question I always ask before replacing an n^2 process with an nlg(n) one is "Am I in that window where n^2 is actually faster?"

</comment><comment> Const-me Ported to C++, added two ATL collections, also added 100M elements data point:

[https://github.com/Const-me/CollectionMicrobench](https://github.com/Const- me/CollectionMicrobench)

Arrays are still generally faster than lists.

The funny thing is Microsoft's linked lists are faster than C++ standard vectors.

<comment><USER27> >The funny thing is Microsoft's linked lists are faster than C++ standard vectors.

If I had to guess, it's because the std::vector is more conservative in memory use and it causes more malloc/array copy calls.

<comment> Const-me I think the main reason is CAtlList class encapsulates its own memory pool. It allocates RAM in batches. The default batch size for CAtlList is 10 elements/batch, user-adjustable in constructor, but I kept the default value 10.

The elements are created directly adjacent to each other. This makes iteration faster because RAM locality despite the pointer-based data structure.

</comment></comment></comment><comment><USER28> There's a simpler and broader point: don't use Big O as the sole means of analysis of a high level language's data structures. The theoretical time/space complexity of a data structure may or may not accurately reflect how it's actually implemented in that language.

</comment><comment><USER29> I just read this nice piece on linked lists and why they have almost no place in programming nowadays

[http://cglab.ca/~abeinges/blah/too-many- lists/book/](http://cglab.ca/~abeinges/blah/too-many-lists/book/)

</comment><comment><USER30> Arrays are fast for getting random elements and inserting/deleting at the end (amortized), but slow for inserting/deleting in the middle.

Linked lists are fast for inserting/deleting/getting at the beginning and end, but slow for random access for anything in the middle.

So far, that's what the article covered. Now if you want fast random insertions/deletions and fast random access, this is possible with balanced trees. A tree-based list can support all single-element operations in O(log n) time. Sample code: [https://www.nayuki.io/page/avl-tree- list](https://www.nayuki.io/page/avl-tree-list)

<comment><USER31> That's not what the article is about nayuki. The author of the article makes and proves a claim that arrays are faster than linked lists regardless of the insertion point. The metrics presented clearly show this.

The reason for this abnormality is cache locality (there are other reasons, which I will not go into right here).

Balanced trees can be pretty slow in fact. After some operations, a tree structure can become quite fragmented in memory, leading to many cache misses. In my experience, it is often faster to work with arrays instead of trees when processing in memory. However, using external storage, a b-tree often introduces quite some performance gain.

</comment></comment><comment><USER32> Here's a question: How often does this _matter_? No, not that big O can fool you. That always matters.

How often does it matter that non-contigouous memory access is slow? Really. How much do those few useconds really matter? In most apps, I would guess that a CPU cache miss isn't noticable by humans.

Yes, non-contiguous structures are significantly slower, but if you don't need to be as fast as possible, eliminating them for that reason only (assuming that there is any non-perf reason to stick with them) is a premature optimization.

But if you ARE optimizing, yeah, you need to think about how often worst-case occurs. Because big O only tells you about worst-case. NOT the average.

<comment><USER33> _A_ cache miss isn't noticeable by a human. Code that cache misses a lot runs 10-100x slower than code that takes into consideration that it's running on a physical machine and not an abstraction. That is very noticeable by humans. Even when your data structures and algos are designed with a nice O(logN), it's very noticeable when one program bogs down with 1/6 the data compared to another.

I work in games, so the story I tell new kids is: The PlayStation2 ran at 300Mhz and a cache miss would cost you 50 cycles. The PlayStation3 ran at 3200Mhz and a cache miss would cost you 500 cycles. So, if you are cache missing a lot, your PS3 game will run as if it were on a PS2.

In other words, not paying attention to cache make your computer run like it's 10 years older than it is. You paid for a modern machine, but you are getting the results of a Craigslist junker. This is true outside of games. It's the reason 4x2Ghz cellphones struggle to run seemingly simple apps. It's a big part of the reason people struggle to orchestrate armies of servers (distributed computing is easier when it's 90% less distributed).

Is it really harder to work with the cache system instead of ignoring it? Yeah, it requires a tiny bit of study an a little bit of planning. In contrast, the theme I see a lot online is to completely dismiss physical reality in favor or theory. And, the theme I see almost universally in the students (and many senior engineers) I interview is complete ignorance of the very existence of cache and it's effects on the code they write. It's very concerning...

<comment><USER32> No, I don't deny it's important to know that cache misses exist, and what they can do: to the contrary, it's vital.

However, in 90% of applications, it's not going to matter, because those applications are spending hundreds of cycles waiting anyways: Disk or network IO, user input, all that stuff is way slower than a cache miss. If you're writing a video game, or a database, or other software with very high soft- realtime speed requirements, or heavy data access, by all means, optimize to avoid cache miss.

But if you're writing a company-internal Rails app, nobody's going to notice, even if you're getting cache miss after cache miss. Which you probably won't.

Actually, if your language isn't compiled, a cache miss is the least of your worries, perf-wise.

And now I've got to see if I can optimize my code to avoid cache misses. But the code's in Scheme, so unless the initial access cost is amortized, I'm already doomed...

<comment><USER34> You're looking at this problem backward. For example, you mention user input. Users may need a second to click or touch a button, but when they do the software should react _instantly_ , and that does not leave you that many cycles. My smartphone's lock screen is my go-to example: most times it fails to follow my finger, and I barely have anything running on it.

Most of the dynamic languages are data and instruction cache-miss machines. They chase objects and pointers all around the memory.

<comment><USER32> >My smartphone's lock screen is my go-to example: most times it fails to follow my finger, and I barely have anything running on it.

...That doesn't sound like a cache miss. Knowing Android, A cache miss is probably the least of your worries.

>Users may need a second to click or touch a button, but when they do the software should react instantly, and that does not leave you that many cycles.

You raise a good point...

>Most of the dynamic languages are data and instruction cache-miss machines. They chase objects and pointers all around the memory.

...and this is part of my point. If you look at a problem and think, "A high- level language is fast enough," then you are implicitly saying that the latency of a cache miss is acceptable. And IME, in most cases that's true. I mean, heck, I'm using Scheme, so while I may have pointer chases like the Amazon has trees, but I CAN optimize them into array lookups, and my code is compiled: not great, but better than most HLLs.

It's the same argument as always: perf vs. development speed. You can be in the C and FP loop, or the Lisp and JS loop.

<comment><USER34> > That doesn't sound like a cache miss. Knowing Android, A cache miss is > probably the least of your worries.

My example was meant to illustrate the user input problem. From what I know about Android, the absymal performance is very much a case of "death from a thousand cuts".

> It's the same argument as always: perf vs. development speed. You can be in > the C and FP loop, or the Lisp and JS loop.

The fast(er) languages we have are old and full of warts, and that makes them slow to develop in. The heavily used HLLs such as Python and Ruby were made by people who did not care much (at all?) about performance, and it shows in many design decisions. But here's the thing: we could have both at the same time. I don't buy this dichotomy.

<comment><USER32> >But here's the thing: we could have both at the same time. I don't buy this dichotomy.

That's actually not true: OO, dynamism, late binding, and a lot of the other things that HLLs have to offer require a lot of pointer chasing and non- consecutive datastructures. I'm mostly a Schemer, and Scheme and Lisp have had decades of research put into making them compile and run fast. Most dynamic languages aren't so lucky. But the required pointer chasing and garbage collection mean they'll never be as fast as C.

Functional programming languages, however, are rarely late-binding, and don't expose as much about their implementation, so some of the pointer chasing can be avoided.

Rust doesn't need a GC, and is fairly C-like - or rather, ALGOL-like and BLISS-like - with added memory safety. So with a programmer who knows what they're doing, it can be pretty fast. But here's the rub: the faster a language is, _the closer it has to be to the metal,_ and the less it can do with high-level features.

So yes, you can make HLLs faster, but you can't take the cache misses out of an HLL, and you can't make a systems language wearing an HLL's clothing - although Rust is making an admirable attempt.

<comment><USER34> > OO, dynamism, late binding

None of these are required for ease of development. At least the first two often result in precisely the opposite.

HLLs are not required to focus on slow abstractions. For example, homogeneous arrays of tagged unions can replace inheritance most of the time. And they avoid breaking your code in 10 files and 20 classes (though for some reason this metric is seen as a good thing way too often).

<comment><USER32> OO, perhaps, but without dynamism and late binding, metaprogramming is difficult, as is any number of techniques. Like, for instance, class generation, and extending methods.

And while I don't set much store by inheritance, it set considerably more store by duck-typing and polymorphism.

>None of these are required for ease of development. At least the first two often result in precisely the opposite.

So are you talking about Java-style OO? Because I was talking about Smalltalk OO, which is pretty different.

 _Required,_ no, but they're tools, and they come in handy. Certainly make development a lot more comfortable.

And some of the time, they make delvelopment a lot easier.

<comment><USER34> > Like, for instance, class generation, and extending methods.

This is precisely the kind of thing that leads to unmaintainable "magic" code, even though it can still be useful but with _extreme_ moderation. So I don't see the point of making that a core feature of any language.

If you have any example to the contrary, I'd love a link to a good open-source project that uses these things extensively.

<comment><USER32> RSpec? A lot of ruby uses metaprogramming in some respect.

As for not seeing the point of making it a feature of your language, how about Lisp? And I'm not just talking macros. Lambdas, late binding, and in Scheme, the ability to rebind anything lead to a lot of cool tricks and capabilities.

And late binding is extrordinarily important.!

<comment><USER34> I don't consider the Ruby ecosystem to be a good example of much. Idiomatic Ruby code is much slower and usually not more maintainable than C++. Actually, it may even be worse thanks to dynamic typing, which makes refactoring much more painful than it already is in large code bases.

Well I guess it's good at making CRUD web sites. Hardly rocket science.

<comment><USER32> Okay then: Lisp.

Tinyclos is a fairly sophisticated implementation of OO and the MOP, written in Scheme.

Its descendants, COOPS, GOOPS, and others, are in most schemes today. Many of them are written in their respective dialect of scheme, with little or no specific compiler support.

SXML allows for writing XML in native scheme syntax.

The anaphoric macros (aif, acond, etc.) are all, well, macros, and thus use metaprogramming principles.

tclOO, [incr tcl], and other OO TCL systems are usually implemented in regular TCL.

Give or take, any large Lisp or Smalltalk codebase takes advantage of dynamic typing, late binding, and some form of metaprogramming.

However, you've made it clear that you hate Ruby, Dynamic Typing, and other such things, as given as much of metaprogramming requires this sort of flexibility, I very much doubt anything I say will convince you that dynamic languages are in any way useful.

<comment><USER34> I don't hate them. I've used python more than once, and will continue to do so. And I think it's great teaching material. It's a good _scripting_ language. But I think its drawbacks far outweigh its advantages for large projects.

All your examples are programming gimmicks, and I've yet to see stuff that solves actual hard problems. I'm not interested in programming for programming's sake. I want to use it to make my computer do _useful_ stuff.

<comment><USER32> Gimmicks? Some of them, yes, but CLOS and its descendants are used in Real applications, as are the TCL OO systems. But if you want Real World, I'll give you real world.

Maxima is a descendant of the original MACSYMA, developed at MIT. It is still a usable and viable system, even if it has ageda bit.

Emacs is a popular programmer's text editor written in C and a dialect of lisp.

Both of the above programs are large, useful, and written in an HLL - <USER2> particularly amenable to pointer chasing, I might add - and they make use of the variety of abstractions which that HLL provides.

If those aren't modern enough for you, check out some Clojure applications.

</comment></comment></comment></comment></comment></comment></comment></comment></comment></comment></comment></comment></comment></comment></comment><comment><USER35> I think the overemphasis of big O, especially during job interviews, is a sad thing.

I think multi-threading is an equally important skill, that gets less attention.

<comment><USER19> It's not. The reason is, a lot of stuff isn't multithreaded or is just a bunch of threads talking to a database. (I've been asked questions about multithreaded stuff, in interviews at companies that specifically did multithreaded stuff. Companies that talked to databases would be better off asking SQL stuff.)

</comment></comment><comment><USER36> One thing I didn't see in the article: in addition to cache misses, the list stores 3 words per word; the array between 1 and 2 (up to 3 only when reallocating,) so array touches less RAM, even when inserting.

</comment><comment><USER37> The author is doing less than 10 insertions per benchmark, that's practically a constant O(1), even if you have to copy the entire array 5 or 10 times don't affect the Big O analysis

<comment><USER38> But he's inserting at the front of an array, which requires the rest of the elements to be copied into a new array. DotNetPerls has a clearer example: [http://www.dotnetperls.com/list-insert](http://www.dotnetperls.com/list- insert)

</comment></comment><comment><USER39> Technically speaking, he ought to be using Big Theta (Θ) to describe his bounds. Throwing Big O around to describe everything is foolish.

</comment><comment><USER40> The benefits of the list abstraction are small---because it's a clumsy, blub- like list abstraction with a container object and iterators.

Look at the code; termination of the loop is even based on an integer count pulled from the container.

A True Scotsman's linked list has no such thing. It's either an empty indicator (like NIL in Lisp) or a binary cell consisting of an item and a pointer to the next one.

The benefit of that abstraction is that you can recurse over it directly without clumsy additional parameters having to be passed.

Another benefit is substructure sharing. We can insert at the front of a list not only in O(1) time and that is great. But perhaps more importantly, existing copies of the list before the insertion _do not change_. And it is the same way if we delete from the front: we just move the local head pointer to the previous node, which doesn't affect anyone else who is still holding on to the pointer to the original front node.

These lists also allow lock-free operation, unlike "heavy weight" containers. Suppose we have a list that acts purely as a container, but is shared by multiple threads. We can insert into it by consing a new node onto the head of the current snapshot of the list, and then doing an atomic-compare-swap to install that head in place of the old head. If it fails, it means the list changed behind our back; we cons the node onto the new list (or rewrite the cons to point to the new one as its "rest" pointer) and try again.

Some of the caching benefits of the array will disappear if the array holds only references/pointers to items. In this example, the containers are typed. The List<int> actually can allocate the int objects in an array that packs them together in memory. Whereas the LinkedList<int> has individual separately allocated nodes which hold the int. Suppose the List and LinkedList hold pointers to heaped objects. Then the impact of caching is softened. It's still the case that multiple pointers in the array can be cached together; but these have to be traversed to access the items themselves. In the case of the LinkedList, we have to traverse a pointer to get to the next node and traverse a pointer to get to the heaped object. But two pointer traversals versus one is not as bad as one traversal versus zero. If the objects being traversed are fairly complex, and have pointers to additional objects that have to be examined during the traversal, it matters even less what kind of list they are accessed from. If I have a list of File objects, for each of which a stream is opened and a regex scan performed, why would I care whether it's an array list or a linked list.

The results shown in this test case tell me that the performances of the two containers are not that far off! Here they are subject to a test case that is designed to highlight the difference between them by making the container content, and its processing, almost as trivial as possible. That 38 seconds versus 51 difference is almost purely in the container-related operations. That is as bad as it gets: from there, the more actual real work you do per container node, the smaller the actual difference. (What is 51 versus 38 in "orders of magnitude"? Why 0.12 orders. It's 0.42 "binary orders of magnitude" (where 1 binary order is a doubling; terminology mine). So in terms of classic Moore's Law (speed doubling every 18 months), that's a 7.6 month advance. "My arrays are 7.6 months ahead of your linked list container, in Moore's Law, in a pure benchmark; eat my dust!"

</comment><comment><USER41> Oprah fooled me once!

</comment><comment><USER42> If the author sees this: please consider changing your color choices. It'd make reading the content you put so much work into producing easier for everyone. I couldn't finish the post. The 90s hacker's lair colors were that offensive.

<comment><USER43> The process of weeding out. Sometimes it's better to select against sensitive people.

<comment><USER44> Why would you select against people with poor vision?

<comment><USER45> The green seems a bit harsh, yes.

But all in all it is a good page for people with poor vision.

No low-contrast and no white background, which are both considered bad on screens.

<comment><USER44> Yes, the white on black is fine. I checked the #8a7ae2 purple ([http://webaim.org/resources/contrastchecker/](http://webaim.org/resources/contrastchecker/)) and, surprisingly, it passes WCAG AAA guidelines. I say "surprisingly" because it hurts my eyes. I think jumping between the alternating white and purple is what does it.

</comment></comment></comment></comment></comment><comment><USER46> This is a great point and I'll probably have to do some performance measurements and change parts of my code now. I should be more careful with sacrificing contiguous storage for O(1) insertions.

It is also worth noting that some manuals say that appending to an array list is O(1) amortized. Which is true, if you make an amortized analysis (which essentially distributes the workload of copying the array into a larger block over all the inserts). That's to keep in mind for systems that have to be realtime or at least produce stable framerates. The worst-case is important and amortized analysis generously glosses over it.

EDIT: Not inserting, appending

<comment><USER9> Insertion into an array list at a uniformly-distributed location is always O(n): you can't avoid moving half the list. Appending is amortized O(1).

<comment><USER47> Not the person you're responding to but perhaps that person meant assuming your insertions are uniformly distributed? Then I think insertion is O(n) amortized..?

At an insertion you'd have

(n + (n-1) + .. + 1)/n = O(n^2) / n = O(n)

The first part comes from each possible run times, each with probability 1/n. You might have to expand the list but that'd also be O(n) amortized.</comment></comment></comment>


How to get over idea block? - <AUTHOR>

I've been reading HN for a few years now, seeing everyone talk about their ideas, their weekend projects, and their startups.  I'm pretty capable with a background in programming and electronics, but for some reason, any time I want to sit down and create or invent something, I just draw a blank.  I'm sure there are other people here who have struggled with the same thing...  How did you get past it?  Thanks! <comment><USER2> I wrote this awhile ago: [http://programmersbreadth.blogspot.com/2011/12/how- internet-...](http://programmersbreadth.blogspot.com/2011/12/how-internet- fried-your-brain.html)

Ideas come from brainstorming - sit somewhere with a notepad and a pen and start writing out ideas - something the average person will be interested in, existing ideas than can be improved, something businesses find useful (like consumer feedback) yet have become accustomed to living without.

Creating something entirely new for a need that does not exist is unheard of. Need drives innovation. A niche I would say is something people have been accustomed to not having. Programming like business is either focused on increasing profits(products or features which are money makers) or reducing cost (saving time, money, effort etc.)

If I could, I'd focus primarily on writing something useful that saves time. Trying to build something that's 'too useful' sometimes makes the process slower than it needs to be.

A simple example of the above-mentioned is inventory management systems (compare selling non-perishables vs selling perishables). An electronics store would use a first-in-first-out approach due to tax benefits. A 'food' store would use a first-expire-first-out approach. The latter approach is quite visible when standing in-front of rows of milk with the first to expire at the front and the latest to expire at the back.

An electronic store could identify electronics by type/brand, while foodstuffs requires another level of precision (a batch - all same ingredients used, same expiration date).

Selecting via type is much easier than selecting via type and batch. 'Foodstuffs' that are used as ingredients generally require some level of trace-ability - which adds another step into the process.

This is where pragmatism (business focused) outshines perfectionism (feature focused).

</comment><comment><USER3> I also am a fountain of ideas, and I've found that this is caused by burn out. My solution is to go do something _completely_ different, that you wouldn't normally do, for a week or so.

Many times I've found that new ideas on a new heightened level start flowing due to the distraction.</comment>


Guitar – Git GUI Client - <AUTHOR> https://github.com/soramimi/Guitar <comment><USER2> I think the author should be applauded for developing a native application instead of yet another electron app. Having a gui for version control is very useful, especially for working out branches, merging etc. The edge cases can always be handled from CLI. For reference, I worked on Perforce for many years and having P4Win instead of the p4 command line was a godsend for most cases because of the hugely complex underlying tool chain.

<comment><USER3> Out of curiosity, why do you prefer a native app over an electron app?

<comment><USER4> I don't understand how this sort of question gets pummeled with downvotes. Having developed Qt and XAML applications, I have to say that the DOM approach makes a lot more sense than wrestling loosely bundles of widgets. In the end Qt apps end up with a widget tree, but the path that they take to get there is convoluted and packed with unnecessary fusion of UI responsibilities and data and even business logic.

<comment><USER5> I don't think end users decide which GUIs they like based on their "DOM approach" any more than they decide what milk they like based on the milking approach.

<comment><USER4> I'm pretty sure that users like good software with pretty GUIs that are easy to use and navigate around and extend.

DOM-based frameworks make this sort of stuff trivially to implement, and some of them evem come with gesture and animation support out-of-the-box. Meanwhile, feel free to take a look at Guitar's source tree to figure out if that is trivial to work with, and at best it looks like an app from 1998.

<comment><USER6> Qt and widget based programs are absolutely not difficult to make. I don't know where this comes from. They are fast and responsive by default instead of laggy and sluggish on a many core xeon loaded with ram. Even using PyQt and embedding python is like a feather compared to electron and that approach is extra easy.

<comment><USER7> But that's not really the point. How easy is it to make great UIs with them?

How easily can you make a staggered fade and slide in animation? Responsively change layout? Apply complex styles?

<comment><USER8> That's a good question but what's the point of all this if no one can use the app if they don't have a minimum of 8gb ram? My wife couldn't run Spotify, slack at the same time cause her machine started swapping.

The developer experience is vastly superior in electron and it's reach is massive. There's no denying. But the underlying problems are pretty serious and the people who complain are not just hating for random reasons.

I hope you get how insane it is when your cpu fan starts spinning because of an app that plays music in the background!

So far the pro electron arguments are concerned only about developer experience who generally have beefier specs in their machines.

The lowest common denominator in end user machines are very different and I request you please keep that in mind!

P.S: I'm a web developer and I fully understand how much valuable electron is for me.

<comment><USER4> > That's a good question but what's the point of all this if no one can use > the app if they don't have a minimum of 8gb ram?

Even vscode, which has a gigantic memory footprint due to its plugin system, requires 1GB to run comfortably.

Nowadays you get computers with 4GB of RAM for 60€.

Let's not bother ourselves with irrelevant details.

<comment><USER9> It's _completely_ relevant. People run more than one application on their desktop computers.

Regardless of what many people say, RAM usage still matters. If every app takes a giant chunk of RAM just to get started ( which many do ) eventually that's going to bite you.

Yes, we do have a ton more resources to work with than we used to, but I'm afraid far too many developers have just stopped caring about it at all -- and that's a very bad thing.

<comment><USER4> > It's completely relevant. People run more than one application on their > desktop computers.

That's great, so that must mean you'll be happy to hear that right now I have two instances of vscodium open, each one running half a dozen plugins, and their total memory footprint is less than 300MB.

Do you have anything relevant to say regarding how much resources are used? Or are we supposed to keep debating if being able to run 40x instances of a text editor is not enough room to work with, or if whether having only 7.8GB of RAM free to run other programs is too restrictive?

</comment></comment></comment></comment></comment></comment></comment></comment></comment></comment></comment><comment><USER10> Being a dad and highly prone to excessive punning if given half a chance, it almost hurts that this is not a GUI for tar.

As a developer I love good GUIs and should check this out.

<comment><USER11> It exists and was active till 2013 cwww.edenwaith.com/products/guitar/

<comment><USER12> If some likes to push a homebrew cask package for it, here is a possible source:

      cask 'gui-tar' do
        version '1.2.4'
        sha256 'e11177b99a01d5cff666f68072fd2145cccda4678a8a98203d97929cbe53dea2'

        url "http://www.edenwaith.com/downloads/guitar.dmg"
        name 'GUI Tar'
        homepage 'http://www.edenwaith.com/products/guitar/'

        app 'GUI Tar.app'
      end

</comment></comment></comment><comment><USER13> I don't like GUI git tools as they always have issues with edge cases. git is a complicated system and you can have multiple weird configurations in your repo/system.

Most of the GUI tools just focus on the MVP workflow and often they crash/bug out on the weird cases.

This is not really an argument against it, but just an observation I made through the years.

Sourcetree is one of the oldest GUI git clients and it has the most stuff handled correctly, and this basically looks like open-source source tree.

<comment><USER14> I really want a hybrid command line/gui git client. Either a GUI app with a terminal front and centre, or a command line tool that has some commands to use UI style things.

I hacked together a bash script that prints out all the branches and lets you click on it to switch to it and it's been pretty useful, just wish I could be bothered to make it properly with more features.

<comment><USER15> GitExtensions includes a terminal and shows the commands it runs and I recently discovered the GitGraph extension in VSCode which allows having a terminal open at the same time. There's more text focused ones like Magit or Tig, I haven't used them personally but from reading the manual of Magit it looks like it can [1] and Tig allows for setting up custom commands [2].

1\. [https://magit.vc/manual/magit/Running-Git- Manually.html](https://magit.vc/manual/magit/Running-Git-Manually.html)

2\. [http://jonas.nitro.dk/tig/manual.html#external- commands](http://jonas.nitro.dk/tig/manual.html#external-commands)

</comment></comment></comment><comment><USER16> As an alternative I highly recommend Fork, a life/time saver when working in Windows, it's a high quality (and native) Git GUI client.

Guitar is also good too, and are thankfully both native!

[0] Fork - [https://fork.dev](https://fork.dev)

<comment><USER17> Fork is closed source, and costs $50.

It is nagware - will work forever but with a nag screen (like WinZip back in the day), and IMO it is definitely worth a one time fee of $50 if you don't mind using closed source.

But it is not quite the same category as Guitar.

<comment><USER18> I actually didn't know that Fork can be bought! I installed a Beta version a while back, upgraded regularly and never have been shown a nag screen. Gladly paid for my license a few minutes ago.

<comment><USER19> Fork is one of the few pieces of software I used in beta where I thought, "You're not asking for money right now, but you seriously need to let me pay you for this."

</comment></comment></comment></comment><comment><USER20> Sublime Merge is this but... sublime: [https://www.sublimemerge.com/](https://www.sublimemerge.com/) it's amazing and pretty similar

<comment><USER21> I've found sublime to be not so sublime. It beats everything in terms of speed but I'm not a fan of the design and it's woefully lacking in customisation and doesn't support a lot of the git options. On Linux it's my only choice for a good GUI but on mac and windows I much prefer fork.dev which is almost as fast and is very feature rich and looks great

<comment><USER20> > fork.dev

That looks cool! Personally I love the simplicity of both Sublime Text and Merge so I gravitate towards those (I don't really care/need about customization)

<comment><USER21> I was doing a trial run of git GUIs cause I'm much more productive with one and I landed on Sublime which was good but lacked a lot of options and I don't think still allows for adding custom commands. I was also extremely miffed when it took months for an update to come out to fix one bug I'd found and was irritating me to no end (I forget the specifics now). For me the update cycle is also extremely slow and the price is prohibitive.

I wish I liked it more but I've settled on fork now. Good you're happy with sublime. I'll check it out again once it's rounded off with more features

</comment></comment></comment></comment><comment><USER22> I'm still using GitX-dev[0], mostly for doing commits and visualizing branches. Although lately I've been committing directly from Visual Studio Code.

For more complex operations nothing beats the CLI.

[0] [https://rowanj.github.io/gitx/](https://rowanj.github.io/gitx/)

<comment><USER23> > For more complex operations nothing beats the CLI.

I think that's only because most of the UIs are so bad! Manipulating a graph is something that I think inherently is actually quite visual.

GitUp[0] has excellent graph-editing facilities, and has replaced almost all of my git command-line machinations.

Alas, it's semi-abandoned and left in a perpetual state of 90% doneness...

A little challenge, if you're interested: Give some git manipulations you would do on the command line, and I will show you how to do it more easily in GitUp, or concede defeat. :-)

[0] [https://github.com/git-up/GitUp](https://github.com/git-up/GitUp)

<comment><USER23> No takers? :-)

</comment></comment></comment><comment><USER24> I have started using lazygit, a CUI, or console user interface. It runs in my terminal window but gives me the same broader perspective a GUI does. Looking at the screenshots of Guitar, which look amazing, I realize with lazygit I get 95% of the benefit of a GUI but in a package that does not disturb my workflow.

[https://github.com/jesseduffield/lazygit](https://github.com/jesseduffield/lazygit)

There are edge cases that lazygit does not handle well, but it is a simple matter to type "q", bop back to the terminal and run whatever git command and get on with life.

<comment><USER25> Which cases does lazygit not handle well? Asking because I'm working on something similar, although probably simpler and with less features [0].

[0] [https://tomas.github.io/jix](https://tomas.github.io/jix)

<comment><USER24> When I think about the situations where I don't use lazygit, it is when I am unsure of what I am doing and therefore unsure what lazygit is doing. Almost everything I normally do with git, I can do with lazygit, but there are things I am not confident about. Squashing commits versus picking and do they go up or down, etc? Rebasing. I tend to go to command-line git in those cases, not because lazygit does not handle them but because I don't. :-)

The crucial advantage of lazygit for me is the "situational awareness" I gain in one window - more than any added functionality. The other take away is for your tools to be utterly trustworthy. :-)

<comment><USER25> Gotcha, thanks.

</comment></comment></comment></comment><comment><USER26> I--for one--love a GUI. Especially for git. I also love a good pun, so I'm definitely going to try this! It's gonna be tough to beat VS-Code and Kraken though.

<comment><USER27> GitKraken has been a godsend, I have been really spoiled by it. Managing big repositories has become a breeze thanks to the visualization techniques it uses. If only the company behind it weren't so questionable about their privacy and monetization policies. I am still on the fence on the product.

<comment><USER28> What issues are there with the privacy policies? Did I miss something?

<comment><USER27> The founder has some controversial views on how to develop the product, this includes more cloud integration.

[https://old.reddit.com/r/git/comments/59j0yq/gitkraken_is_no...](https://old.reddit.com/r/git/comments/59j0yq/gitkraken_is_no_longer_free_freak_out_or_dont/)

On top of that GitKraken is closed-source and in the past there were concerns on telemetry collection: [https://old.reddit.com/r/git/comments/4dgpgd/gitkraken_data_...](https://old.reddit.com/r/git/comments/4dgpgd/gitkraken_data_kraken_yet_another_application/)

Nothing major, which is why I am still using it, but definitely enough to make you wary.

</comment></comment></comment></comment><comment><USER29> From all the git GUI tools I like VS Code the most. I know it is IDE, but it is very fast, free, open source, multi-platform, and has amazing plugins for most use cases.

Other than that, most frequently I use CLI.

<comment><USER30> I also find IntelliJ embedded git client to do wonders too.

</comment></comment><comment> v-yadli Why isn't this name used for a tar gui frontend? :p

<comment><USER31> I love it. But we've got

        man tar

</comment></comment><comment><USER32> Thank you to the author for making a native application.

</comment><comment><USER33> I've been using Tower (on Mac) for a long time and I'm happy with it. It's also free now if you are a student: [https://www.git- tower.com/students](https://www.git-tower.com/students)

<comment><USER34> And if you are not a student it's a subscription: _every year_ you'll pay up 69 or 99 €, "access to Tower ends when the most recently paid-up subscription period ends."

Not so great... (they switched to subscription some years ago, adios amigos!)

<comment><USER35> Tower is quite literally the only application I'm willing to pay subscription pricing for. It's quality software, doesn't hold my data hostage, and has a steady stream of updates. I'm as _adios amigos_ as you, but I don't think the usual reasons apply here.

</comment></comment></comment><comment><USER36> Nice tool, will be testing it. Thanks for your hard work

</comment><comment><USER37> Still haven't found anything better than gitk

<comment><USER38> That is nice. [https://git-scm.com/docs/gitk](https://git-scm.com/docs/gitk)

</comment></comment><comment><USER39> Based on one of the screenshots, it looks like Guitar doesn't support word diffing. Sourcetree lacks word diffing as well and has had an issue about it since 2012: [https://jira.atlassian.com/browse/SRCTREE-888](https://jira.atlassian.com/browse/SRCTREE-888)

<comment><USER40> Which GUI clients that you know of do support word diffing?

<comment><USER39> Fork ([https://git-fork.com/](https://git-fork.com/))

<comment><USER40> I have Fork, but I don't see any word diffing functionality, unless you are just referring to an additional highlight on the changed word(s)

<comment><USER39> Yes, that's what I'm referring to.

You can see it in Fork's screenshot: [https://git- fork.com/images/diffViewer.jpg](https://git-fork.com/images/diffViewer.jpg)

But if you look at Guitar's, you'll see the lack of word diffing, and that makes it take significantly longer to see exactly what changed: [https://camo.githubusercontent.com/3c6c338ece93afda723cef575...](https://camo.githubusercontent.com/3c6c338ece93afda723cef575c0c8004502bb758/68747470733a2f2f736f72616d696d692e6769746875622e696f2f4775697461722f68746d6c2f73637265656e73686f74732f6d61636f732e706e67)

<comment><AUTHOR> Already requested for _Guitar_. Follow this issue thread.[0]

[0] [https://github.com/soramimi/Guitar/issues/76](https://github.com/soramimi/Guitar/issues/76)

<comment><USER39> Thanks!

</comment></comment></comment></comment></comment></comment></comment><comment><USER41> Haiku is a nice welcome.

<comment><USER42> Though IMO using Qt for its GUI instead of the native lightweight Haiku ones is missing the point of Haiku a bit.

</comment></comment><comment><USER43> Cool effort and props for the native app, but I've been using GitKraken for over 2 years now. I know all the console commands I'll need because I've used them for so long, but I can't be bothered with them anymore. GUI and merge editor is so nice to have.

</comment><comment><USER44> I never really felt a need to use a GUI client for Git. I think they all hinder understanding of Git instead of helping. However I use Tig (console client) extensively for read-only Git operations. For everything else, there is Magit (Emacs Git client).

</comment><comment><USER17> OT

Is there a way to connect Guitar to a remote server.

Or, are there other Git GUI's that work over SSH?

I can use VSCode, but it is not really a Git GUI, and doesn't satisfy my needs.

<comment><USER45> depending on your OS, there are ways to mount a remote ssh location locally, which means that you can use any GUI you want (but it will be slow).

for example: [https://www.redhat.com/sysadmin/sshfs](https://www.redhat.com/sysadmin/sshfs)

</comment></comment><comment><USER46> is there any comparison to existing git clients? I am currently happy with git extensions and KDiff (for resolving conflicts)

</comment><comment><USER47> Sadly no (portable) ZIP package for Windows :(

<comment><AUTHOR> If you really need it — create feature request.[0]

FTR, Actually installers for Windows available only for Win32[1], so you may want request Win64 builds too.

[0] [https://github.com/soramimi/Guitar/issues](https://github.com/soramimi/Guitar/issues)

[1] [https://files.soramimi.jp/guitar/](https://files.soramimi.jp/guitar/)

</comment></comment><comment><USER48> aur/guitar-git. Just used it to grab its own source from Github. Love it. This stays.

</comment><comment><USER49> Just use the cli - it's faster and more productive</comment>