Search Unity

PSA and call for help. Il2CPP Metadata taking up huge amount of RAM

Discussion in 'Scripting' started by Dmitry_Kozlovtsev, Jan 16, 2020.

  1. Dmitry_Kozlovtsev

    Dmitry_Kozlovtsev

    Joined:
    Mar 2, 2018
    Posts:
    13
    Hello everyone!

    In our current, quite large project, we've encountered an unexpected problem, il2cpp metadata (IL2CppClass structures and things it contains) take up at least 100 mb of RAM used. I've tracked this memory to metadata allocation via both Visual Studio and XCode memory profiler.

    I've written a simple cpp code to try to inspect are all classes have the same amount of metainformation or it depends on the class. I've managed to account ot approximatelly 60 mb's of metainformation in this code (I'm sure I did not get it all). I'll post the cpp code

    Code (CSharp):
    1. long size_of_methodInfo(std::set<const MethodInfo*> alreadyCountedMethods, const MethodInfo* info)
    2. {
    3.     if (info == 0 || alreadyCountedMethods.count(info) > 0)
    4.     {
    5.         return 0;
    6.     }
    7.     alreadyCountedMethods.insert(info);
    8.  
    9.     long result = 0;
    10.     if (info->is_inflated == true && info->is_generic == false)
    11.     {
    12.         if (info->rgctx_data != 0)
    13.         {
    14.             result += sizeof(Il2CppRGCTXData);
    15.             //result += size_of_methodInfo(alreadyCountedMethods, info->rgctx_data->method);
    16.         }
    17.     }
    18.     else
    19.     {
    20.         if (info->methodDefinition != 0)
    21.         {
    22.             result += sizeof(Il2CppMethodDefinition);
    23.         }
    24.     }
    25.  
    26.  
    27.     if (info->is_inflated == true)
    28.     {
    29.         if (!info->is_generic && info->genericMethod != 0)
    30.         {
    31.             result += sizeof(Il2CppGenericMethod);
    32.             result += size_of_methodInfo(alreadyCountedMethods, info->genericMethod->methodDefinition);
    33.         }
    34.     }
    35.     else if (info->is_generic)
    36.     {
    37.         if (info->genericContainer != 0)
    38.         {
    39.             result += sizeof(Il2CppGenericContainer);
    40.         }
    41.     }
    42.  
    43.     result += info->parameters_count * sizeof(ParameterInfo);
    44.     return result;
    45. }
    46.  
    47. void GartherMetadataSizeInfo()
    48. {
    49.     auto snapshot = il2cpp::vm::MemoryInformation::CaptureManagedMemorySnapshot();
    50.     std::ofstream myfile;
    51.     myfile.open("C:\\Metadata\\Data.txt");
    52.     std::set<const MethodInfo*> methodInfoSet = std::set<const MethodInfo*>();
    53.  
    54.     for (int i = 0; i < snapshot->metadata.typeCount; ++i)
    55.     {
    56.         auto type = snapshot->metadata.types[i];
    57.  
    58.         Il2CppClass* classPtr = (Il2CppClass*)type.typeInfoAddress;
    59.         long size = sizeof(Il2CppClass);
    60.         long uniqueMethodInfosSize = 0;
    61.         if (classPtr->typeDefinition != 0)
    62.         {
    63.             size += sizeof(Il2CppTypeDefinition);
    64.         }
    65.  
    66.         if (classPtr->interopData != 0)
    67.         {
    68.             size += sizeof(Il2CppInteropData);
    69.         }
    70.  
    71.         size += classPtr->method_count * static_cast<uint32_t>(sizeof(void*));
    72.         size += classPtr->method_count * sizeof(MethodInfo);
    73.         for (int idx = 0; idx < classPtr->method_count; ++idx)
    74.         {
    75.             const MethodInfo* methodInfo = classPtr->methods[idx];
    76.             uniqueMethodInfosSize += size_of_methodInfo(methodInfoSet, methodInfo);
    77.         }
    78.  
    79.         size += classPtr->event_count * static_cast<uint32_t>(sizeof(void*));
    80.         size += classPtr->event_count * sizeof(EventInfo);
    81.         for (int idx = 0; idx < classPtr->event_count; ++idx)
    82.         {
    83.             EventInfo eventInfo = classPtr->events[idx];
    84.             uniqueMethodInfosSize += size_of_methodInfo(methodInfoSet, eventInfo.add);
    85.             uniqueMethodInfosSize += size_of_methodInfo(methodInfoSet, eventInfo.raise);
    86.             uniqueMethodInfosSize += size_of_methodInfo(methodInfoSet, eventInfo.remove);
    87.         }
    88.  
    89.         size += classPtr->property_count * static_cast<uint32_t>(sizeof(void*));
    90.         size += classPtr->property_count * sizeof(PropertyInfo);
    91.         for (int idx = 0; idx < classPtr->property_count; ++idx)
    92.         {
    93.             PropertyInfo propertyInfo = classPtr->properties[idx];
    94.             uniqueMethodInfosSize += size_of_methodInfo(methodInfoSet, propertyInfo.get);
    95.             uniqueMethodInfosSize += size_of_methodInfo(methodInfoSet, propertyInfo.set);
    96.         }
    97.  
    98.         size += classPtr->interface_offsets_count * static_cast<uint32_t>(sizeof(void*));
    99.         size += classPtr->interface_offsets_count * sizeof(Il2CppRuntimeInterfaceOffsetPair);
    100.  
    101.         size += classPtr->field_count * static_cast<uint32_t>(sizeof(void*));
    102.         size += classPtr->field_count * sizeof(FieldInfo);
    103.  
    104.         size += classPtr->vtable_count * static_cast<uint32_t>(sizeof(void*));
    105.         size += classPtr->vtable_count * sizeof(VirtualInvokeData);
    106.  
    107.         size += classPtr->rgctx_data == 0 ? 0 : sizeof(Il2CppRGCTXData);
    108.        
    109.         size += classPtr->interfaces_count * static_cast<uint32_t>(sizeof(void*));
    110.         size += classPtr->nested_type_count * static_cast<uint32_t>(sizeof(void*));
    111.  
    112.         if (classPtr->generic_class != 0)
    113.         {
    114.             auto generic_class = classPtr->generic_class;
    115.         }
    116.  
    117.  
    118.         myfile << type.assemblyName << "\t";
    119.         myfile << type.name << "\t";
    120.         myfile << uniqueMethodInfosSize << "\t";
    121.         myfile << size << "\t";
    122.         myfile << size + uniqueMethodInfosSize << std::endl;
    123.     }
    124.  
    125.     myfile.close();
    126.     il2cpp::vm::MemoryInformation::FreeCapturedManagedMemorySnapshot(snapshot);
    127. }
    The results are interesting. The more methods and fields and properties the class has the more metainformation it requires and more memory it occupies; generics require slightly more memory than ordinaty classes; arrays for some reason are champions of metainformation size(there is a two-dimentional array in the project that has at least 200kb of metainformation). In our project most of the metainformation is generated by various generic specializations(generic collections and unirx are the main culprits here).

    So innocent looking c# code produces code that is extremely memory hungry just to support its execution.
    And now it is time for a call for help
    1. Have someone encountered this problem and found some kind of a solution?
    2. Can someone from Unity comment on this and maybe investigate ways to reduce memory consumption?
     
    iteterin likes this.
  2. JoshPeterson

    JoshPeterson

    Unity Technologies

    Joined:
    Jul 21, 2014
    Posts:
    6,926
    Most of this memory for metadata (although not all) is lazily allocated when it is used. If you are seeing all or most of the metadata actually allocated, that sometimes indicates there is code in the project which uses reflection to look at all types in all assemblies (or something similar). Often we've seen code which looks for a certain attribute on all types, for example.

    When code like that executes, the VM must allocate memory for the metadata to correctly implement reflection APIs, and that memory is never deallocated. If there is any code like that in this project, it might be good to scope down the reflection to only the assemblies which could have that attribute for example. Maybe avoid using reflection to inspect base class library assemblies like mscorlib.dll, System.dll, etc.
     
    Dmitry_Kozlovtsev and iteterin like this.
  3. Altair4Ru

    Altair4Ru

    Joined:
    Aug 21, 2018
    Posts:
    2
    Hi, Josh.

    We're on the same team with Dmitry, so I'll be talking about the same project.

    Surely we do have reflection in a couple of modules. One of them (json serialization) is being used extensively throughout the whole runtime session. But, if we look into Instruments, we won't see any huge calls in terms of memory allocations. Instead, there are multiple not-so-big allocations with individual callstacks here and there. From my standpoint it shows that reflection is not the culprit.

    In the end, we have a lot of code (custom and engine) that instantiates metadata at common use. These metadata chunks along with hashmaps containing these chunks sum into something like 100Mb of native memory that we can't handle at all. The only way we have to lower metadata memory footprint is to write/use less types/methods/generics.

    Do you have any suggestions?
     
  4. JoshPeterson

    JoshPeterson

    Unity Technologies

    Joined:
    Jul 21, 2014
    Posts:
    6,926
    We don't have good tools for visibility into the metadata allocations IL2CPP does at the moment. This is something that we're aware is lacking, and we're planning to improve the situation (both by trying to make metadata allocations more efficient, and by providing better visibility into them) soon.

    For the time being, "use less code or reflection" is the best advice I can give, which is probably not too helpful.