Did you consider to make inline the default behaviour?

#1

Disclaimer:
First of all I’m not a language designer but just a “junior” application developer. But I want to give you my viewpoint because I know advanced developers have difficultys of thinking like beginners.

Topic:
If I refactor some piece of code to a new method/function, it’s often for readability, and the ability to find that specific code easier in the future so that I can modify it, it’s part of the dividing a big problem into smaller problems idea. Reusability is the secondery benefit, which I get in theory.
What I don’t have in mind is the heap (trust me I don’t even know what exactly that is), or anything else happening in background. In my abstraction creating the new method does exactly what inline also does -> copying and pasting the code, just the over way around (well in the past I didn’t know that that’s my abstraction, I thought that this happens in reality). Secondery, inlining gives a performance boost (not always a big one, intelij says it’s not significant, but every small performance boost is welcome, especially if you are a junior and your known tools for optimizing are limited). So why not think about making inlining the default behaviour? I understand that long functions are bad for inlining because the size of code will rise. But first of all, what we have learned is that functions/methods should have single purposes. Single purpose functions shouldn’t tend to be long, so having long functions should be avoided often, why make the default behaviour (not inline) supporting the bad habbit (long functions)? Also even if a function is long, only if it gets called at separate places the inline should be a problem. A long function called just once shouldn’t take more place if I understand it right. This again shows that default inlining would be a better especially because I naively think that long functions are doing so many specific thinks at once that they aren’t reusable anyways, so they won’t get called at many places. And If you have such a specific candidate, you could simply mark it with a “don’t inline” keyword. If I’m right, inlining doesn’t work with recursion because you can’t paste code into itself, I didn’t try it I don’t know if you get a compiler error but in that case you could simply(?) make the rule “inline by default, except if there is recursion in that function”. I know that compile time is also a criteria but I don’t know if Inlining slows down compiling or not.

You can say that I’m wrong and point out my mistakes that they I could learn, otherwise if I’m correct, maybe I gave you a hint to improve the language.

#2

Inline has too many disadvantages to be the default. You can no longer us private or protected functions, since inline functions can only use other functions of the same visibility level as they are.

As you pointed out recursion is no longer possible. While it might be possible for the compiler to know which functions to mark no-inline when only one function is calling itself but what happens in a function chain like this a -> b -> c -> d -> e -> a? I guess you could report an error at compile time but I imagine this could get annoying fast. I don’t think this is very common but when it does happen it would be very hard to solve properly.

The next problem is polymorphism/inheritance. You can’t override a function of a library if the it is already inlined somewhere. This would break a lot of the concepts behind object oriented programming.

Then there is the problem of the size of your program. An important reason for using functions (instead of just inlining them) is the size of your executable file. Let’s say you call a function 100 different times. Then it takes 100 more times the space when you inline it compared to a normal function. This get’s even worse since the function is probably very big, since all the function it uses get inlined as well.
This would also impact the performance negatively. The biggest impact is on startup times since all this extra code has to be loaded by the JVM, but I guess some optimizations are harder to make as well.

Then there is the point of your minor performance gain. In most cases it is so small that you can’t even measure it. For it to have a real effect you would probably need to call the same function a hundred thousand times, but in that case the JVM will just inline it at runtime.

Inline only really has 2 advantages. One is when passing lambdas as you no longer need to create an extra class for each lambda. This is where the real performance gain from inline functions comes from.
The other advantage of inline functions are reified types.

2 Likes
#3

Very good answer @Wasabi375 , but you forgot one thing.

The biggest advantage of inline functions in my opinion is that they allow non-local return statements within a lambda argument. This enables to write APIs that feel like language constructs.

1 Like
#4

Well to be fair, I was trying to list the disadvantages of inline not its advantages :stuck_out_tongue_winking_eye:

#5

You should be more careful how you start your last paragraph then :face_with_hand_over_mouth:

1 Like
#6

I would like to repeat and emphasize one of @Wasabi375 's arguments, because this one alone leads the OP’s argument ad absurdum.

The OP first admits that long functions should not be inlined. Then he argues that in an ideal world all functions are small, and can safely be inlined. But by inlining them by default, the functions will become longer and longer, which conflicts with the premise of not inlining long functions.

1 Like
#7

@linkot take a look here https://en.wikipedia.org/wiki/Optimizing_compiler
Moreover a JIT compiler is able to inline virtual methods at runtime (in some circumstances).

#8

In fact, in most. Some time ago I was testing performance benefits from kotlin inlining. Well, in most cases it is strongly negative. JVM’s own inlining is much more effective.

#9

@Wasabi375
In the case of private and protected, it’s simple, this keywords could disable inlining.

The recursion problem: I must admit that I don’t know if it’s solvable in anyway. And by solving, I mean detecting then to not inline (or in other words detecting complex recursion), again like at the part with private and protected if it’s possible to detect it, than the compiler could simply not inline that part. This points makes sense.

Libraries:
I forgot to write it, I had that in mind, libraries would need to use the “do not inline” keyword excessively that’s a drawback I see it to. But what I didn’t thought about is the problem with overriding functions, that seems like the most important argument. I was more concerned that the creator of the librarie couldn’t know how often his function will be called so he shouldn’t use inline to not make the programs using the library to big. Which brings us to the next point:
Size. Yes that’s why I said make inline the default, I didn’t say make it the only option. Adding a dont inline keyword to a function I know I’m calling often wouldn’t be worse than adding a inline to a function where I’m passing lambdas.

#10

@fatjoe79

Well you take away the abstraction. Short functions are good because they are readable, you understand what the code does at that specific part. No matter what happens in the background no matter if it’s inlined or not. If you would bring your own argument further and take more of the abstraction away, in the end a program is more or less just one long sequence of bits right? No matter if you use many functions or not. But abstraction is good so we use functions, more abstraction is better so we use many functions with single purposes no matter what happens in the background, no matter if they get merged to one long function or not. If the long function is more performant, just do it in the background, we need the abstraction and readability only until we start the compiler, after that performance is our first priority.

Also it’s about reusability. Think about function A is calling B, B is calling C and C is calling D and so on. You can reuse function C, and if function C comes in with inlined function D and E, that’s not bad since function C needs them. But if all this functions there not inlined functions, but real parts of a single long function A, you couldn’t reuse C. You could only call function A but that is doing things you don’t need or want (like doing the stuff of function B).

In conclusion even if it becomes one long single function in the end, having small functions gives you reusability and readability. The abstraction haves a purpose which shouldn’t be ignored. Functions are abstraction, I don’t create a function to start a orchestra of activitys in the background (which cost performance) but just to organise code in readable and reusable way.

But than again @darksnake said that he measured negative impact, so it doesn’t matter at all, I was assumming that it gives a non measurable but still existing performance gain, which could add up if it would be the default and used more often.

#11

Yes, but inline gains you basically nothing. In some cases longer functions (due to inlining) might even cost you performance, when it becomes too costly to analyze the code for performances at runtime.
The costs of inlining functions is higher than the possible gain from it. Therefor I don’t think it’s a good idea set inline as the default behavior. It would just force developers to think about inlining for each function they write and spent far more time than is necessary for this, especially if what @darksnake said is true and inlining has a negative effect on performance.

IMO there is only one reason why you would use inline (except for lambdas and reified types) and this is providing wrappers around other libraries, e.g an extension function that provides functionality originally in a static function. fun Double.toRadians() = Math.toRadians(this)

#12

My argument is not about abstraction. It is about code size. Automatically inlining all functions will bloat the code size, even if the inlined functions are small at the beginning. Consider this example:

fun main() {
  o(1)
  o(2)
  o(3)
}

fun o(i: Int) {
  a(i+1)
  a(i+2)
}

fun a(i: Int) {
  b(i+1)
  c(i+2)
  b(i+3)
}

fun b(i: Int) {
  c(i+1)
  d(i+1)
}

fun c(i: Int) {
  d(i+1)
  d(i+2)
}

fun d(i: Int) {
  print(i+1)
  print(i+2)
}

All functions are fairly short and simple. So they are all good candidates for inlining? Let’s see what happens when we inline all functions except main and print:

fun main() {
  print(1+1+1+1+1+1)
  print(1+1+1+1+1+2)
  print(1+1+1+1+2+1)
  print(1+1+1+1+2+2)
  print(1+1+1+1+1)
  print(1+1+1+1+2)

  print(1+1+2+1+1)
  print(1+1+2+1+2)
  print(1+1+2+2+1)
  print(1+1+2+2+2)

  print(1+1+3+1+1+1)
  print(1+1+3+1+1+2)
  print(1+1+3+1+2+1)
  print(1+1+3+1+2+2)
  print(1+1+3+1+1)
  print(1+1+3+1+2)

  print(1+3+1+1+1+1)
  print(1+3+1+1+1+2)
  print(1+3+1+1+2+1)
  print(1+3+1+1+2+2)
  print(1+3+1+1+1)
  print(1+3+1+1+2)

  print(1+3+2+1+1)
  print(1+3+2+1+2)
  print(1+3+2+2+1)
  print(1+3+2+2+2)

  print(1+3+3+1+1+1)
  print(1+3+3+1+1+2)
  print(1+3+3+1+2+1)
  print(1+3+3+1+2+2)
  print(1+3+3+1+1)
  print(1+3+3+1+2)


  print(2+1+1+1+1+1)
  print(2+1+1+1+1+2)
  print(2+1+1+1+2+1)
  print(2+1+1+1+2+2)
  print(2+1+1+1+1)
  print(2+1+1+1+2)

  print(2+1+2+1+1)
  print(2+1+2+1+2)
  print(2+1+2+2+1)
  print(2+1+2+2+2)

  print(2+1+3+1+1+1)
  print(2+1+3+1+1+2)
  print(2+1+3+1+2+1)
  print(2+1+3+1+2+2)
  print(2+1+3+1+1)
  print(2+1+3+1+2)

  print(2+3+1+1+1+1)
  print(2+3+1+1+1+2)
  print(2+3+1+1+2+1)
  print(2+3+1+1+2+2)
  print(2+3+1+1+1)
  print(2+3+1+1+2)

  print(2+3+2+1+1)
  print(2+3+2+1+2)
  print(2+3+2+2+1)
  print(2+3+2+2+2)

  print(2+3+3+1+1+1)
  print(2+3+3+1+1+2)
  print(2+3+3+1+2+1)
  print(2+3+3+1+2+2)
  print(2+3+3+1+1)
  print(2+3+3+1+2)


  print(3+1+1+1+1+1)
  print(3+1+1+1+1+2)
  print(3+1+1+1+2+1)
  print(3+1+1+1+2+2)
  print(3+1+1+1+1)
  print(3+1+1+1+2)

  print(3+1+2+1+1)
  print(3+1+2+1+2)
  print(3+1+2+2+1)
  print(3+1+2+2+2)

  print(3+1+3+1+1+1)
  print(3+1+3+1+1+2)
  print(3+1+3+1+2+1)
  print(3+1+3+1+2+2)
  print(3+1+3+1+1)
  print(3+1+3+1+2)

  print(3+3+1+1+1+1)
  print(3+3+1+1+1+2)
  print(3+3+1+1+2+1)
  print(3+3+1+1+2+2)
  print(3+3+1+1+1)
  print(3+3+1+1+2)

  print(3+3+2+1+1)
  print(3+3+2+1+2)
  print(3+3+2+2+1)
  print(3+3+2+2+2)

  print(3+3+3+1+1+1)
  print(3+3+3+1+1+2)
  print(3+3+3+1+2+1)
  print(3+3+3+1+2+2)
  print(3+3+3+1+1)
  print(3+3+3+1+2)
}

I doubt that the minuscule gain in time performance (if there even is a measurable one) is worth the exponential growth of code size. (Yes, the plus operator would be evaluated at compile time and I omitted that in my example, but it does not take away from my argument)

2 Likes
#13

While your point is correct and valid, the math in your example would probably get eliminated because they only involve constants. If those used variables it would emphasize the point more.

#14

He actually mentioned that in his post. Also in any real world example this would most likely be some other kind of expression that the compiler can’t simply optimize away.