On Sun, 4 Aug 2024 22:51:33 GMT, John Hendrikx <jhendr...@openjdk.org> wrote:

>> I don't understand this argument at all. Why would it be reasonable to 
>> _only_ build a more efficient architecture _if_ there's such a huge problem 
>> with the "naive" alternative that this alone would be readily apparent? If 
>> taken to the extreme, this line of thinking leads to death by a thousand 
>> papercuts, no single issue big enough that it's a problem all by itself, but 
>> taken together it amounts to sluggish performance.
>> 
>> Here's the thing: this code could reasonably be executed tens of thousands 
>> of times per second. Think of a complex user interface that changes its 
>> visual state for many nodes at once. What you seem to perceive as a 
>> micro-optimization is actually a part of an architecture that involves many 
>> types in the `Background` and `Border` area.
>> 
>> The basic idea is: if I can rely on `interpolate()` returning existing 
>> instances (either the start value or the end value) if nothing has changed, 
>> then the "parent object's" interpolate method can use an identity comparison 
>> (which is basically for free) to choose whether to return itself (or the end 
>> value), or return a new instance.
>> 
>> Let's look at the `Border.interpolate()` implementation:
>> 
>> @Override
>> public Border interpolate(Border endValue, double t) {
>>     Objects.requireNonNull(endValue, "endValue cannot be null");
>>     if (t <= 0) return this;
>>     if (t >= 1) return endValue;
>> 
>>     // interpolateListsPairwise() is implemented such that it returns 
>> existing list instances
>>     // (i.e. the 'this.images' or 'endValue.images' arguments) as an 
>> indication that the result
>>     // is shallow-equal to either of the input arguments. This allows us to 
>> very quickly detect
>>     // if we can return 'this' or 'endValue' without allocating a new Border 
>> instance.
>>     List<BorderImage> newImages = images == endValue.images ?
>>         images : InterpolationUtils.interpolateListsPairwise(images, 
>> endValue.images, t);
>> 
>>     List<BorderStroke> newStrokes = strokes == endValue.strokes ?
>>         strokes : InterpolationUtils.interpolateListsPairwise(strokes, 
>> endValue.strokes, t);
>> 
>>     if (images == newImages && strokes == newStrokes) {
>>         return this;
>>     }
>> 
>>     if (endValue.images == newImages && endValue.strokes == newStrokes) {
>>         return endValue;
>>     }
>> 
>>     return new Border(newStrokes, newImages);
>> }
>> 
>> 
>> `interpolateListsPairwise()` _already knows_ whether both lists are equal, 
>> which makes the `return this` and `return endValue` branches after the 
>> interp...
>
> I think you said that you didn't actually test this, so we don't know if this 
> will perform better. It's based on an assumption of what Java will do, and on 
> assumptions what is "bad" and what is "good".  Without having tested this, 
> you are optimizing blind.  I would be entirely satisfied if you had said "I 
> tested this with a few thousand transitions, the allocation turned out to be 
> a hot spot, so I optimized it which is the reason this code looks a bit out 
> of the ordinary".
> 
> I also would have liked a comment explaining any pitfalls introduced by the 
> optimization; the use of `==` by callers is unusual, and a crucial piece of 
> information when I see code like:
> 
>         if (secondList.isEmpty()) {
>             return firstList.isEmpty() ? firstList : secondList;
>         }
> 
> It would deserve at least an explanation that you're specifically returning 
> one of the passed in arguments so callers can do reference equality checks.
> 
> Java's JIT is very finicky.  For example, an earlier comment asked if the 
> first `if` case is the most common one.  The assumption being that this will 
> improve performance.  However, you can't make that assumption at all (I've 
> done tests that show it's almost impossible to predict which "if order" will 
> be faster on the JVM); re-arranging a function or "unrolling" it can have 
> large negative consequences if that means the JIT will no longer inline your 
> function.
> 
> Now, I will give you that this **probably** will be faster, but I'm making an 
> assumption here... I really can't be sure.  That still leaves some 
> explanatory comments that I think you should add with the assumptions that 
> were made for this optimization.
> 
>>  If taken to the extreme, this line of thinking leads to death by a thousand 
>> papercuts, no single issue big enough that it's a problem all by itself, but 
>> taken together it amounts to sluggish performance.
> 
> No, when hunting for performance problems, it will be readily apparent what 
> total process is taking up all the time, and how often parts of it are being 
> called and how much time they take in total.  Put a profiler on it, and see 
> where the actual time is being spent on a case with a lot of transitions if 
> you want to optimize it.  Even the JVM itself does this, not bothering to 
> even compile code that isn't executed at least a certain amount of times, or 
> not bothering to further optimize code after compilation if that code isn't 
> in a hot path.
> 
> What optimization generally does is it that it makes code harder to 
> understand (almost unavoid...

I think what is being done in this PR is fine, even though your arguments 
@hjohn are perfectly valid.  One feature that I think has value is the fact 
that this design tries to limit new allocations, and I think it's a good thing.

One interesting point you bring up is that we have no good performance test for 
CSS.  What would represent a good performance test?  Create a thousand of 
labels and measure the time it takes to update?  Does it matter if it's a label 
or something else?  And do we have a reliable way to measure?  What do you 
think?

-------------

PR Review Comment: https://git.openjdk.org/jfx/pull/1522#discussion_r1705961254

Reply via email to