Skip to content
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

Java enumerations should *cache* the field values #1243

Open
jonpryor opened this issue Aug 14, 2024 · 2 comments · May be fixed by #1248
Open

Java enumerations should *cache* the field values #1243

jonpryor opened this issue Aug 14, 2024 · 2 comments · May be fixed by #1248
Labels
enhancement Proposed change to current functionality generator Issues binding a Java library (generator, class-parse, etc.)
Milestone

Comments

@jonpryor
Copy link
Member

jonpryor commented Aug 14, 2024

Consider the Thread.State enum:

/* partial */ class Thread {
    public static final /* partial */ enum State {
        NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED
    }
}

This "actually" is a class with fields for each enum value:

% javap java.lang.Thread.State
Compiled from "Thread.java"
public final class java.lang.Thread$State extends java.lang.Enum<java.lang.Thread$State> {
  public static final java.lang.Thread$State NEW;
  public static final java.lang.Thread$State RUNNABLE;
  public static final java.lang.Thread$State BLOCKED;
  public static final java.lang.Thread$State WAITING;
  public static final java.lang.Thread$State TIMED_WAITING;
  public static final java.lang.Thread$State TERMINATED;
  public static java.lang.Thread$State[] values();
  public static java.lang.Thread$State valueOf(java.lang.String);
  static {};
}

When we bind it, we bind the fields as properties:

public sealed partial class State : Java.Lang.Enum
{
internal State() : base (default(Java.Interop.JniObjectReference), default(Java.Interop.JniObjectReferenceOptions)) { }
public static Java.Lang.Thread.State Blocked { get { throw null; } }
[System.ComponentModel.EditorBrowsableAttribute(1)]
[System.Diagnostics.DebuggerBrowsableAttribute(0)]
public override Java.Interop.JniPeerMembers JniPeerMembers { get { throw null; } }
public static Java.Lang.Thread.State New { get { throw null; } }
public static Java.Lang.Thread.State Runnable { get { throw null; } }
public static Java.Lang.Thread.State Terminated { get { throw null; } }
public static Java.Lang.Thread.State TimedWaiting { get { throw null; } }
public static Java.Lang.Thread.State Waiting { get { throw null; } }
[Java.Interop.JniMethodSignatureAttribute("valueOf", "(Ljava/lang/String;)Ljava/lang/Thread$State;")]
public static Java.Lang.Thread.State ValueOf(string name) { throw null; }
[Java.Interop.JniMethodSignatureAttribute("values", "()[Ljava/lang/Thread$State;")]
public static Java.Interop.JavaObjectArray<Java.Lang.Thread.State>? Values() { throw null; }
}

However, each of those properties involves a ValueManager lookup:

namespace Java.Lang {
	public partial class Thread {
		public sealed partial class State : global::Java.Lang.Enum {
			public static global::Java.Lang.Thread.State? Runnable {
				get {
					const string __id = "RUNNABLE.Ljava/lang/Thread$State;";

					var __v = _members.StaticFields.GetObjectValue (__id);
					return global::Java.Interop.JniEnvironment.Runtime.ValueManager.GetValue<global::Java.Lang.Thread.State? >(ref __v, JniObjectReferenceOptions.Copy);
				}
			}
		}
	}
}

This is JavaInterop1, not XAJavaInterop1, but .NET for Android isn't much different:

			public static Java.Lang.Thread.State? Runnable {
				get {
					const string __id = "RUNNABLE.Ljava/lang/Thread$State;";

					var __v = _members.StaticFields.GetObjectValue (__id);
					return global::Java.Lang.Object.GetObject<Java.Lang.Thread.State> (__v.Handle, JniHandleOwnership.TransferLocalRef);
				}
			}

The problem is that value lookup is not fast -- identity hash code needs to be obtained, locks obtained, etc. -- to the point that repeated enum lookups can actually show up in profiles.

TODO: @jonathanpeppers, please provide your profile data. :-D

Suggestion: could we update property generation for all final fields to cache the return value? This means we'd only need to call StaticFields.GetObjectValue() and "GetValue" once, instead of once per-access:

			global::Java.Lang.Thread.State? _Runnable_cache;
			public static global::Java.Lang.Thread.State? Runnable {
				get {
					if (_Runnable_cache != null) return _Runnable_cache;
					const string __id = "RUNNABLE.Ljava/lang/Thread$State;";

					var __v = _members.StaticFields.GetObjectValue (__id);
					return _Runnable_cache  = global::Java.Interop.JniEnvironment.Runtime.ValueManager.GetValue<global::Java.Lang.Thread.State? >(ref __v, JniObjectReferenceOptions.Copy);
				}
			}
@jonpryor
Copy link
Member Author

Updated description to mention that we should do caching for all final fields, not just fields of enum types. That's what final is for, after all, right?

@jonathanpeppers
Copy link
Member

Here is an example trace:

Search for either Android.Graphics.Paint.Cap.get_Butt() or Android.Graphics.Paint.Join.get_Miter() as an example.

I'm doing this for now, which would also help .NET 8:

@jpobst jpobst added enhancement Proposed change to current functionality generator Issues binding a Java library (generator, class-parse, etc.) labels Aug 15, 2024
@jpobst jpobst linked a pull request Aug 27, 2024 that will close this issue
jonpryor added a commit that referenced this issue Sep 4, 2024
Context: #1243
Context: #1248

Java's `final` keyword is contextual, and maps to (at least?) three
separate keywords in C#:

  * `const` on fields
  * `readonly` on fields
  * `sealed` on types and methods

When binding fields, we only support "const" `final` fields: fields
for which the value is known at compile-time.

Non-`const` fields are bound as properties, requiring a lookup for
every property access.

This can be problematic, performance-wise, as `final` fields without
a compile-time value only need to be looked up once; afterward, their
value cannot change [^1].  As such, we should consider altering our
binding of "readonly" static properties to *cache* the value.

PR #1248 implemented a "nullable"-based approach to caching the field
value.  While this approach works for reference types, it is likely
not thread safe for `int?` and other value types.

[There is a comment on #1248 to make the approach thread-safe][0],
but @jonpryor isn't entirely sure if it's correct.  The
"straightfoward" approach would be to use a C# `lock` statement,
but that requires a GC-allocated lock object, which would increase
memory use.  Furthermore, if this code is wrong, the only way to fix
it is by regenerating the bindings.

@jonpryor considered moving the thread-safety logic into a separate
type, moving it outside of the generated code.  This is implemented
as `ReadOnlyProperty<T>`, in this commit.

To help figure this out, along with the performance implications,
add a `ReadOnlyPropertyTiming` test fixture to
`Java.Interop-PerformanceTests.dll` to measure performance, and
update `JavaTiming` to have the various proposed binding ideas so
that we can determine the resulting code size.

Results are as follows:

| Approach                                              | Code Size (bytes) | Total (s) | Amortized (ticks) |
| ----------------------------------------------------- | ----------------: | --------: | ----------------: |
| No caching (current)                                  |                21 | 0.0029275 |              2927 |
| "nullable" caching (not thread-safe; #1248 approach)  |                65 | 0.0000823 |                82 |
| Inlined thread-safe caching                           |                48 | 0.0000656 |                65 |
| `ReadOnlyProperty<T>` caching                         |        24+17 = 41 | 0.0001644 |               164 |

Worst performance is to not cache anything.  At least the expected
behavior is verified.

"Nullable" caching is quite performant.  Pity it isn't thread-safe.

"Inlined thread-safe caching" is ~20% faster than "nullable" caching.

`ReadOnlyProperty<T>` caching is nearly 2x slower than "nullable".

Can `ReadOnlyProperty<T>` be made faster?

[0]: #1248 (comment)

[^1]: Not strictly true; *instance* fields can change within the
      object constructor, and *static* fields change change within
      the static constructor.  As #1248 is about static fields of
      *bound* types, there should be no way for us to observe this.
      Things become trickier with instance fields.
jonpryor added a commit that referenced this issue Sep 4, 2024
Context: #1243
Context: #1248

Java's `final` keyword is contextual, and maps to (at least?) three
separate keywords in C#:

  * `const` on fields
  * `readonly` on fields
  * `sealed` on types and methods

When binding fields, we only support "const" `final` fields: fields
for which the value is known at compile-time.

Non-`const` fields are bound as properties, requiring a lookup for
every property access.

This can be problematic, performance-wise, as `final` fields without
a compile-time value only need to be looked up once; afterward, their
value cannot change [^1].  As such, we should consider altering our
binding of "readonly" static properties to *cache* the value.

PR #1248 implemented a "nullable"-based approach to caching the field
value.  While this approach works for reference types, it is likely
not thread safe for `int?` and other value types.

[There is a comment on #1248 to make the approach thread-safe][0],
but @jonpryor isn't entirely sure if it's correct.  The
"straightfoward" approach would be to use a C# `lock` statement,
but that requires a GC-allocated lock object, which would increase
memory use.  Furthermore, if this code is wrong, the only way to fix
it is by regenerating the bindings.

@jonpryor considered moving the thread-safety logic into a separate
type, moving it outside of the generated code.  This is implemented
as `ReadOnlyProperty<T>`, in this commit.

To help figure this out, along with the performance implications,
add a `ReadOnlyPropertyTiming` test fixture to
`Java.Interop-PerformanceTests.dll` to measure performance, and
update `JavaTiming` to have the various proposed binding ideas so
that we can determine the resulting code size.

Results are as follows:

| Approach                                              | Code Size (bytes) | Total (s) | Amortized (ticks) |
| ----------------------------------------------------- | ----------------: | --------: | ----------------: |
| No caching (current)                                  |                21 | 0.0029098 |              2909 |
| "nullable" caching (not thread-safe; #1248 approach)  |                65 | 0.0000827 |                82 |
| Inlined thread-safe caching                           |                48 | 0.0000664 |                66 |
| `ReadOnlyProperty<T>` caching                         |        19+21 = 40 | 0.0001548 |               154 |

Worst performance is to not cache anything.  At least the expected
behavior is verified.

"Nullable" caching is quite performant.  Pity it isn't thread-safe.

"Inlined thread-safe caching" is ~20% faster than "nullable" caching.

`ReadOnlyProperty<T>` caching is nearly 2x slower than "nullable".

Can `ReadOnlyProperty<T>` be made faster?

[0]: #1248 (comment)

[^1]: Not strictly true; *instance* fields can change within the
      object constructor, and *static* fields change change within
      the static constructor.  As #1248 is about static fields of
      *bound* types, there should be no way for us to observe this.
      Things become trickier with instance fields.
@jpobst jpobst added this to the .NET 10 milestone Sep 11, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement Proposed change to current functionality generator Issues binding a Java library (generator, class-parse, etc.)
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants