Skip to content

Commit

Permalink
Speed up java-to-managed typemap lookups
Browse files Browse the repository at this point in the history
Up until now, Xamarin.Android used string comparison when finding a
Managed type corresponding to a given Java type.  Even though the
strings were pre-sorted at build time, multiple string comparisons
costed more time than necessary.  To improve comparison speed, this
commit implements lookups based on hash values (using the `xxHash`
algorithm) calculated for all the Java names at build time.  This allows
us to process each Java type once at run time - to generate its hash.
After that, the hash is used to binary search an array of hashes and the
result (if found) is an index into array with the appropriate
Java-to-Managed mapping.

This change also allows us to move Java type names from the mapping
structure (`TypeMapJava`) to a separate array.  We used to keep Java
type name in the structure to make matching slightly faster, but it
required unnecessarily complicated structure size calculation at
runtime, so that binary search can properly work on an array of
`TypeMapJava` structures whose size would differ from application to
application (and sometimes even between builds).  The change also saves
space, because when the Java type name was stored in the structure, all
the structures had to have the same size, and thus all type names
shorter than the longest one had to be padded with NUL characters.

A handful of other optimizations are implemented as well.  Namely:

  * the `JNIEnv.RegisterJniNatives` method is now called
    directly (thanks to the `[UnmanagedCallersOnly]` attribute) when
    running under .NET6
  * a simpler binary search function was implemented
  * the `typemap_managed_to_java` and `typemap_java_to_managed` internal
    calls are now registered directly from the `EmbeddedAssemblies`
    class instead of from the `MonodroidRuntime` class
  * a number of native functions are now forcibly inlined
  * a number of native functions are now static instead of instance

Startup performance gains vary depending on where we look.  The
`Displayed` time sees changes that are negligible, however the most
affected area of the startup sequence (the call to `JNIEnv.Initialize`)
which registers types and involves the biggest number of lookups sees
improvements of up to 12%. The changes will also positively affect
application performance after startup:

On Pixel 3 XL running Android 12:

| Before | After  | Δ        | Notes                                          |
| ------ | ------ | -------- | ---------------------------------------------- |
| 14.967 | 13.586 | -9.23% ✓ | preload enabled; 32-bit build                  |
| 15.312 | 14.343 | -6.33% ✓ | preload enabled; 32-bit build; no compression  |
| 13.577 | 12.792 | -5.78% ✓ | preload enabled; 64-bit build                  |
| 13.677 | 12.894 | -5.73% ✓ | preload disabled; 64-bit build; no compression |
| 13.601 | 12.838 | -5.61% ✓ | preload disabled; 64-bit build                 |
| 13.656 | 12.953 | -5.15% ✓ | preload enabled; 64-bit build; no compression  |
| 14.638 | 14.070 | -3.88% ✓ | preload disabled; 32-bit build                 |
| 15.053 | 14.526 | -3.50% ✓ | preload disabled; 32-bit build; no compression |

On Pixel 6 XL running Android 12:

| Before | After | Δ         | Notes                                          |
| ------ | ----- | --------- | ---------------------------------------------- |
| 8.972  | 7.826 | -12.78% ✓ | preload enabled; 32-bit build                  |
| 8.833  | 7.823 | -11.43% ✓ | preload enabled; 32-bit build; no compression  |
| 8.611  | 8.031 | -6.74% ✓  | preload disabled; 32-bit build; no compression |
| 6.533  | 6.104 | -6.57% ✓  | preload disabled; 64-bit build; no compression |
| 6.504  | 6.119 | -5.92% ✓  | preload enabled; 64-bit build; no compression  |
| 6.426  | 6.052 | -5.83% ✓  | preload disabled; 64-bit build                 |
| 6.493  | 6.125 | -5.67% ✓  | preload enabled; 64-bit build                  |
| 8.446  | 8.088 | -4.23% ✓  | preload disabled; 32-bit build                 |
  • Loading branch information
grendello committed Apr 7, 2022
1 parent 3f2e55d commit fdbd7ef
Show file tree
Hide file tree
Showing 14 changed files with 473 additions and 263 deletions.
3 changes: 3 additions & 0 deletions src/Mono.Android/Android.Runtime/JNIEnv.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,9 @@ internal static bool ShouldWrapJavaException (Java.Lang.Throwable? t, [CallerMem
[DllImport ("libc")]
static extern int gettid ();

#if NETCOREAPP
[UnmanagedCallersOnly]
#endif
static unsafe void RegisterJniNatives (IntPtr typeName_ptr, int typeName_len, IntPtr jniClass, IntPtr methods_ptr, int methods_len)
{
string typeName = new string ((char*) typeName_ptr, 0, typeName_len);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -477,10 +477,10 @@ public void WriteStructureArray<T> (StructureInfo<T> info, IList<StructureInstan
);

arrayOutput.WriteLine ("[");
string fieldIndent = $"{Indent}{Indent}";
for (int i = 0; i < count; i++) {
StructureInstance<T> instance = instances[i];

arrayOutput.WriteLine ($"{Indent}; {i}");
WriteStructureBody (info, instance, bodyWriterOptions);
if (i < count - 1) {
arrayOutput.Write (", ");
Expand Down Expand Up @@ -508,6 +508,76 @@ public void WriteStructureArray<T> (StructureInfo<T> info, IList<StructureInstan
WriteStructureArray<T> (info, instances, LlvmIrVariableOptions.Default, symbolName, writeFieldComment, initialComment);
}

public void WriteArray (IList<string> values, string symbolName)
{
WriteEOL ();
WriteEOL (symbolName);

ulong arrayStringCounter = 0;
var strings = new List<StringSymbolInfo> ();

foreach (string s in values) {
string name = WriteUniqueString ($"__{symbolName}", s, ref arrayStringCounter, LlvmIrVariableOptions.LocalConstexprString, out ulong size);
strings.Add (new StringSymbolInfo (name, size));
}

if (strings.Count > 0) {
Output.WriteLine ();
}

WriteStringArray (symbolName, LlvmIrVariableOptions.GlobalConstantStringPointer, strings);
}

public void WriteArray<T> (IList<T> values, LlvmIrVariableOptions options, string symbolName, Func<int, T, string?>? commentProvider = null) where T: struct
{
bool optimizeOutput = commentProvider == null;

WriteGlobalSymbolStart (symbolName, options);
string elementType = MapManagedTypeToIR (typeof (T), out ulong size);
Output.WriteLine ($"[{values.Count} x {elementType}] [");
Output.Write (Indent);
for (int i = 0; i < values.Count; i++) {
if (i != 0) {
if (optimizeOutput) {
Output.Write (',');
if (i % 8 == 0) {
Output.WriteLine ($" ; {i - 8}..{i - 1}");
Output.Write (Indent);
} else {
Output.Write (' ');
}
} else {
Output.Write (Indent);
}
}

Output.Write ($"{elementType} {values[i]}");

if (!optimizeOutput) {
bool last = i == values.Count - 1;
if (!last) {
Output.Write (',');
}

string? comment = commentProvider (i, values[i]);
if (!String.IsNullOrEmpty (comment)) {
Output.Write ($" ; {comment}");
}

if (!last) {
Output.WriteLine ();
}
}
}
if (optimizeOutput && values.Count / 8 != 0) {
int idx = values.Count - (values.Count % 8);
Output.Write ($" ; {idx}..{values.Count - 1}");
}

Output.WriteLine ();
Output.WriteLine ($"], align {GetAggregateAlignment ((int)size, size * (ulong)values.Count)}");
}

void AssertArraySize<T> (StructureInfo<T> info, StructureMemberInfo<T> smi, ulong length, ulong expectedLength)
{
if (length == expectedLength) {
Expand Down Expand Up @@ -911,7 +981,7 @@ public void WriteNameValueArray (string symbolName, IDictionary<string, string>
WriteEOL ();
WriteEOL (symbolName);

var strings = new List<(ulong stringSize, string varName)> ();
var strings = new List<StringSymbolInfo> ();
long i = 0;
ulong arrayStringCounter = 0;

Expand All @@ -923,19 +993,31 @@ public void WriteNameValueArray (string symbolName, IDictionary<string, string>
WriteArrayString (value, $"v_{i}");
i++;
}

if (strings.Count > 0) {
Output.WriteLine ();
}

WriteGlobalSymbolStart (symbolName, LlvmIrVariableOptions.GlobalConstantStringPointer);
WriteStringArray (symbolName, LlvmIrVariableOptions.GlobalConstantStringPointer, strings);

void WriteArrayString (string str, string symbolSuffix)
{
string name = WriteUniqueString ($"__{symbolName}_{symbolSuffix}", str, ref arrayStringCounter, LlvmIrVariableOptions.LocalConstexprString, out ulong size);
strings.Add (new StringSymbolInfo (name, size));
}
}

void WriteStringArray (string symbolName, LlvmIrVariableOptions options, List<StringSymbolInfo> strings)
{
WriteGlobalSymbolStart (symbolName, options);
Output.Write ($"[{strings.Count} x i8*]");

if (strings.Count > 0) {
Output.WriteLine (" [");

for (int j = 0; j < strings.Count; j++) {
ulong size = strings[j].stringSize;
string varName = strings[j].varName;
ulong size = strings[j].Size;
string varName = strings[j].SymbolName;

//
// Syntax: https://llvm.org/docs/LangRef.html#getelementptr-instruction
Expand All @@ -961,12 +1043,6 @@ public void WriteNameValueArray (string symbolName, IDictionary<string, string>
Output.Write ("]");
}
Output.WriteLine ($", align {GetAggregateAlignment (PointerSize, arraySize)}");

void WriteArrayString (string str, string symbolSuffix)
{
string name = WriteUniqueString ($"__{symbolName}_{symbolSuffix}", str, ref arrayStringCounter, LlvmIrVariableOptions.LocalConstexprString, out ulong size);
strings.Add (new (size, name));
}
}

/// <summary>
Expand Down
23 changes: 2 additions & 21 deletions src/Xamarin.Android.Build.Tasks/Utilities/NativeTypeMappingData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,21 @@ namespace Xamarin.Android.Tasks
class NativeTypeMappingData
{
public TypeMapGenerator.ModuleReleaseData[] Modules { get; }
public IDictionary<string, string> AssemblyNames { get; }
public string[] JavaTypeNames { get; }
public TypeMapGenerator.TypeMapReleaseEntry[] JavaTypes { get; }

public uint MapModuleCount { get; }
public uint JavaTypeCount { get; }
public uint JavaNameWidth { get; }

public NativeTypeMappingData (Action<string> logger, TypeMapGenerator.ModuleReleaseData[] modules, int javaNameWidth)
public NativeTypeMappingData (Action<string> logger, TypeMapGenerator.ModuleReleaseData[] modules)
{
Modules = modules ?? throw new ArgumentNullException (nameof (modules));

MapModuleCount = (uint)modules.Length;
JavaNameWidth = (uint)javaNameWidth;

AssemblyNames = new Dictionary<string, string> (StringComparer.Ordinal);

var tempJavaTypes = new Dictionary<string, TypeMapGenerator.TypeMapReleaseEntry> (StringComparer.Ordinal);
int managedStringCounter = 0;
var moduleComparer = new TypeMapGenerator.ModuleUUIDArrayComparer ();

foreach (TypeMapGenerator.ModuleReleaseData data in modules) {
data.AssemblyNameLabel = $"map_aname.{managedStringCounter++}";
AssemblyNames.Add (data.AssemblyNameLabel, data.AssemblyName);

int moduleIndex = Array.BinarySearch (modules, data, moduleComparer);
if (moduleIndex < 0)
throw new InvalidOperationException ($"Unable to map module with MVID {data.Mvid} to array index");
Expand All @@ -44,16 +34,7 @@ public NativeTypeMappingData (Action<string> logger, TypeMapGenerator.ModuleRele
}
}

var javaNames = tempJavaTypes.Keys.ToArray ();
Array.Sort (javaNames, StringComparer.Ordinal);

var javaTypes = new TypeMapGenerator.TypeMapReleaseEntry[javaNames.Length];
for (int i = 0; i < javaNames.Length; i++) {
javaTypes[i] = tempJavaTypes[javaNames[i]];
}

JavaTypes = javaTypes;
JavaTypeNames = javaNames;
JavaTypes = tempJavaTypes.Values.ToArray ();
JavaTypeCount = (uint)JavaTypes.Length;
}
}
Expand Down
21 changes: 3 additions & 18 deletions src/Xamarin.Android.Build.Tasks/Utilities/TypeMapGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,18 +40,9 @@ public int Compare (ModuleReleaseData left, ModuleReleaseData right)
}
}

internal sealed class TypeMapEntryArrayComparer : IComparer<TypeMapReleaseEntry>
{
public int Compare (TypeMapReleaseEntry left, TypeMapReleaseEntry right)
{
return String.CompareOrdinal (left.JavaName, right.JavaName);
}
}

internal sealed class TypeMapReleaseEntry
{
public string JavaName;
public int JavaNameLength;
public string ManagedTypeName;
public uint Token;
public int AssemblyNameIndex = -1;
Expand All @@ -67,7 +58,6 @@ internal sealed class ModuleReleaseData
public TypeMapReleaseEntry[] Types;
public Dictionary<uint, TypeMapReleaseEntry> DuplicateTypes;
public string AssemblyName;
public string AssemblyNameLabel;
public string OutputFilePath;

public Dictionary<string, TypeMapReleaseEntry> TypesScratch;
Expand Down Expand Up @@ -343,7 +333,6 @@ string GetManagedTypeName (TypeDefinition td)
bool GenerateRelease (bool skipJniAddNativeMethodRegistrationAttributeScan, List<TypeDefinition> javaTypes, string outputDirectory, ApplicationConfigTaskState appConfState)
{
int assemblyId = 0;
int maxJavaNameLength = 0;
var knownAssemblies = new Dictionary<string, int> (StringComparer.Ordinal);
var tempModules = new Dictionary<byte[], ModuleReleaseData> ();
Dictionary <AssemblyDefinition, int> moduleCounter = null;
Expand Down Expand Up @@ -392,16 +381,12 @@ bool GenerateRelease (bool skipJniAddNativeMethodRegistrationAttributeScan, List
// a Java type name to a managed type. This fixes https://github.com/xamarin/xamarin-android/issues/4660
var entry = new TypeMapReleaseEntry {
JavaName = javaName,
JavaNameLength = outputEncoding.GetByteCount (javaName),
ManagedTypeName = td.FullName,
Token = td.MetadataToken.ToUInt32 (),
AssemblyNameIndex = knownAssemblies [assemblyName],
SkipInJavaToManaged = ShouldSkipInJavaToManaged (td),
};

if (entry.JavaNameLength > maxJavaNameLength)
maxJavaNameLength = entry.JavaNameLength;

if (moduleData.TypesScratch.ContainsKey (entry.JavaName)) {
// This is disabled because it costs a lot of time (around 150ms per standard XF Integration app
// build) and has no value for the end user. The message is left here because it may be useful to us
Expand All @@ -415,19 +400,19 @@ bool GenerateRelease (bool skipJniAddNativeMethodRegistrationAttributeScan, List
var modules = tempModules.Values.ToArray ();
Array.Sort (modules, new ModuleUUIDArrayComparer ());

var typeMapEntryComparer = new TypeMapEntryArrayComparer ();
foreach (ModuleReleaseData module in modules) {
if (module.TypesScratch.Count == 0) {
module.Types = Array.Empty<TypeMapReleaseEntry> ();
continue;
}

// No need to sort here, the LLVM IR generator will compute hashes and sort
// the array on write.
module.Types = module.TypesScratch.Values.ToArray ();
Array.Sort (module.Types, typeMapEntryComparer);
}

NativeTypeMappingData data;
data = new NativeTypeMappingData (logger, modules, maxJavaNameLength + 1);
data = new NativeTypeMappingData (logger, modules);

var generator = new TypeMappingReleaseNativeAssemblyGenerator (data);
generator.Init ();
Expand Down
Loading

0 comments on commit fdbd7ef

Please sign in to comment.