One reason why bloat happens (not the only one) is that when we increase the number of requirements from N to 2N, the program size S does not simply go to 2S.

Sometimes when we add a requirement, numerous complications cause new lines of code to be added throughout the program. So the requirement doesn't have a fixed cost in terms of additional size; the bigger the program already is, the more lines are added by the new requirement.

Someone wants rational numbers in the numeric tower. Okay, so we have to implement addition of a rational to every existing type, including itself. Multiplication of a rational by every existing type as well as itself. Subtraction, division, ad nauseum. The cost in terms of code size for the requirement to have rationals depends on how many numeric types we already have.

Development by accretion of third-party code is the major cause of bloat. Third party components are already getting bloated over time due to the global bloat effect. And here you are, just mashing these components together to make your program. You don't use anywhere near the full functionality of these components. You link that entire five megabyte shared library because it has three useful functions. The seventy-seven others you don't use have to stay there because it's a shared library; some other program might attach to it and use a different subset.

These components get upgraded over time. Security fixes, enhancements, loss of support for old versions. Several years later, you're still just using only three useful functions in that shared library, and none of them have changed. But it's no longer five megabytes; it's now ten and has 157 other functions that you're not using, not 77. You hadn't even done any work to get that five more megabytes of bloat; you just triggered some automatic update a bunch of times over several years and ran some regression tests that passed.

Even the same code gets bigger when compiled. Thirty years ago, the functions were packed together, as well as their internal basic blocks: there was no advantage to any special alignment for branch targets. Loops were not unrolled and code wasn't inlined much: caches weren't big enough so it was often counterproductive. C++ templates came into the picture since then: let's generate umpteen variants of the identical code into slightly different machine language dependent on type.

Managed languages: these drag entire platforms with them. And the internals are "not user serviceable": you generally don't rebuild these things to remove what you don't want. You accept the platform with all its bells and whistles and work your app into that. Their bloat is nothing new. In the 1980's, some Lisp implementations were criticized for making 20 megabyte images (at a time when that really hurt). The rest of the world caught up to that: we now have non-Lisp languages that have lexical closures, garbage collection and ... huge images, far in excess of 20 megabytes. Today's managed languages, being platforms, have naturally copied the concept of package management from actual operating system platforms. It's super easy to bloat up the image just essentially by adding simple declarations of what you want in it, chosen from a large online catalog.