-
-
Notifications
You must be signed in to change notification settings - Fork 116
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Inline all logging functions #34
Comments
The main issue with inline methods is that they don't play nice with logging of line numbers. |
The main issue with not inlining methods is that it defeats the purpose this library. Or is there an advantage other than lazy string evaluation? Could you please elaborate on your concerns about line numbers and inheritance? |
From my experience kotlin-logging methods on the stack has neglectable effect. So I don't think it defeats the purpose of this library. If you think otherwise please give more details. |
I'm skeptical that it's negligible. How are you measuring negligible? Maybe string evaluations would also be negligible... The main reason inline functions exist is to improve performance by avoiding the overhead of creating unnecessary lambdas. That's what it says in the first paragraph of the official documentation you linked to.
I agree this is a concern. But this only happens when the logger type is declared to be the interface. And in that case it works like a normal method. The same way your library works now. There is no downside. However there is a potential upside. When the logger is declared as the class, its methods will be inlined. I understand the choice you made to go with the factory pattern. It's a solid design. But don't be too idealistic. Performance is advertised as a central focus of this library. I would consider some other options.
My intuition tells me that the only line number context that should be lost might be the line numbers of the code in the inline method. But I may have reproduced what you ran into. Log4j throws an error at StackLocator.calcLocation, so nothing is logged. Looks like a compatibility issue. I wonder how hard it would be to make this work. I feel like it should in theory be possible, but it may require changes to kotlin. This problem only appears if the logger field is declared as LocationAwareKLogger. If I declare it as KLogger, it works fine, because the inline methods don't actually get inlined. I assume that we wouldn't run into any issues like this with LocationIgnorantKLogger. That's the only place where performance really matters anyway; no one is logging line numbers if they care about performance. So you could just make the methods inline for LocationIgnorantKLogger and keep regular methods in LocationAwareKLogger. |
Few notes about performance:
Assuming we'll have inline methods for |
I did performance testing with different inlining ways here. Please feel free to recheck this on your machines. For current moment:
So class+method inlining works at least 50% faster (at 1.5 times!!). In this I think we can consider adding special inlined version for at least JVM version. @oshai, I create create PR with this option. What do you think about this improvement (please note - old option will be still available)? |
@imanushin thanks for the details analysis! |
For current moment KLogging facades slf4 for JVM and Marker for JS. Inline class insists on the one implementation only. In this case we have the following options:
Just to summarize the fact: solution "inline classes and inline methods" improves the performance, however it couldn't work for Kotlin Common modules. Unfortunately... |
I really like the idea of #112 |
@imanushin do you know if the limitation of common modules and inline method "relaxed" now with Kotlin 1.4? |
I'd also like full inline API for this, but I can see how this is problematic. I believe it should be inline, after all, even if it should break binary compatibility. |
@oshai, this is looks like interesting. However I thing I found the way to keep the backward compatibility. We can remain the same signature, however add new - with no inline function call. This increases memory traffic in comparison with the most optimal solution, however in addition it allows us to decrease bytecode amount. Example:
|
@imanushin it's an interesting solution. Maybe warnNoInline can have reduced visibility somehow? |
As I said in #112, I think @oshai And there are 2 problems with adding new flavour here. 1 By chasing the 'unopinionated' tag, the library becomes schizophrenic. It splits the user base. This doubles the API surface, maintenance (this cost trickles down to users as well), documentation, complicates explanation of the library by having to explain not only each flavour, but right in the get-go overload user with low-lvl concepts in order to explain pros and cons of each flavor so he can choose one. The user just wants to log the damned thing, preferably learning the library in 1 min by skimming the README file. Look at the mess with testing frameworks. Kotest and scalatest are both great projects, but I refuse to read/study pages like https://github.com/kotest/kotest/blob/master/doc/styles.md or https://www.scalatest.org/user_guide/selecting_a_style. I'm not sure all the flavors are really necessary and 1 I still struggle with using even the one I always use (because the libs have me forever confused about it) and 2 this may lead to the need for even greater modularity of the library (like scalatest, which broke itself last year to split itself by flavor - that was fun for everyone). In the end, users only lose. No idiomatic use. Suddenly swiss army knife. More documentation, more API, more classes, more artifacts, more code, bigger sizes, etc. Also, adding more API is not a way to avoid binary incompatibility. If it should broken, it should break. 2 Provided that another flavor is a good idea, the flavor changes should be purely syntactical - but they are not! Trade-off flavors are evil. Are you going to tell users to choose readability vs performance? Promise performance boosts with certain flavors? Are you going to back those promises up with facts? Uphold those promises across language versions, platforms, etc? Will you be documenting this and keep updating the documentation? Will someone do comprehensive testing comparing the flavors? Or will the documentation simply state "Flavor X should be slower than Y, but nobody knows due to too many factors and no benchmarks - just use it how you like." I feel like adding another flavor suddenly ups the requirements even on the existing code. This is not criticism, its an appeal to carefully let the ideas mature before potentially committing to something with consequences. Personally, I dont know the innards of the library to dare to point to the 'best' solution, so I at least want to help by brainstorming about ideas. But thinking about it: when I think about this library I think of simple and does the job - and I like that about it. |
@sghpjuikit I totally agree with the concerns raised in 1+2 and I don't plan to rush into implementing it. So I think we can decide on few directions right now:
I generally tend to 1, but would love to get some more feedback or suggestions how to inline without breaking other things (mainly the line numbers logging). |
I'd like to add my voice to the "let's use inline functions" faction as well. I think this is the perfect use case for inline functions. I'm not sure what happens to line numbers in stack traces, but that's something the Kotlin compiler team has to figure out, isn't it? As far as I know, this already works out-of-the-box with Kotlin 1.3 or above, inline or no. That being said, I personally would prefer this:
... over this:
... simply due to the fact that I really want to use the built-in string templates in Kotlin and having to use something else (e.g. |
Can you please clarify this? |
I use Kotlin extensively at work and there are also inline functions involved. I've yet to encounter a stacktrace where the line numbers didn't match the source code, so I'm not really sure line number mismatches are an issue when using inline functions. There might be a case I haven't encountered yet though. |
Also, performance issue oshai/kotlin-logging#34
@MartinHaeusler , may I ask you to advice some kind of design here? I created draft PR, please check #226. Precisely please check this line - https://github.com/MicroUtils/kotlin-logging/pull/226/files#diff-0b429ddafa82b8ceaaa5c94a88dfd47b370051bf951c90cb5d89889a94a3a392R50 So what we have in general:
So, do you know how to correct function signature to have pretty syntax here? |
I think what you're aiming for is:
Is that correct? I see what you tried to do, but there is a better way. Consider this: inline fun org.slf4j.Logger.info(msg: () -> Any?){
if(this.isInfoEnabled){
this.info(msg())
}
} If you call this extension method like this: val log = LoggerFactory.getLogger("mylogger")
log.info { "Hello World" } // note the curly braces ... then the kotlin compiler will automatically inline the val log = LoggerFactory.getLogger("mylogger")
if(log.isInfoEnabled){
log.info("Hello World")
} The lambda doesn't exist at runtime anymore, so there is no overhead. Also, the inline function itself is gone, so it's just as good as using I hope that helps. |
@MartinHaeusler , inline logging functions aren't work well, as @oshai stated here - #34 (comment) . And yes, you had got my concept fully. So for now we have two contradicting opinions:
If we use extension methods for that case then user will be able to choose what they want. (e.g. if you want to optimize code - use extension methods, if you want to have precise logging location - use methods from interface). However functionality will be duplicated. @oshai , what is your opinion, should we add extension methods for inline lambdas purpose (so |
I think that what logging frameworks did nicely is that they separated the api from the decision if you want to log line numbers, and it's only in configuration. I saw other approaches that are adding line numbers on compile-time (flogger), but I don't know if that is possible with the kotlin compiler. |
Instead of fiddling with line numbers manually, wouldn't it be smarter to report the issue to the Kotlin team at JetBrains and do some lobbying work there to ensure that the line numbers get fixed? |
I think that with K2 this whole discussion is pointless, as K2 generates lambda functions using invokedynamic. An statet in this comment it is already statet, that invokedynamic with lambdas is cheaper then string concatination without it. |
It seems that the whole fuzz around logging is to not create "expensive"
String
object, if logging level is not enabled. That totally makes sense.And that's why we library offers a nice function:
So, what gets generated for this line?
Hm... instead of a
String
, we create:elementType
That doesn't look very cheap to me.
On the other hand, in Kotlin StdLib pretty much all
map
orforEach
functions areinline
. So, we might write it like this:And this gets compiled to the simplest possible, desired lazy evaluation of String:
Just like we wanted in the first place.
The text was updated successfully, but these errors were encountered: