LoginSignup
1
0

MethodImplOptions(インライン展開など)によるIL2CPPのふるまい

Last updated at Posted at 2023-12-13

前書き

この記事は、2023のUnityアドカレの12/14の記事です。
今年は、完走賞に挑戦してみたいと思います。Qiita君ぬい欲しい!

はじめに

プログラミングにおける必須機能として、「関数呼び出し」というものがあるのは皆さまご存知でしょう。しかし、関数呼び出しにはコストがかかります。

なので、コンパイラによって関数の「呼び出し」を関数の中身に置き換えてしまう最適化を「インライン展開」といいます。

.NET Frameworkや.NET(CoreCLR)では、メソッドに[MethodImpl(MethodImplOptions.AggressiveInlining)]をつけることで、インライン展開されやすくなるらしいです。つけたからといって必ずインライン展開されるとは限らず、何らかの条件付きのようです。(条件についてははっきりしたことが分かっていないので、ソースも読んでみたいですね)

メモ

INLINE_OBSERVATION

src/coreclr/jit/inline.h#L165
enum class InlineObservation
{
#define INLINE_OBSERVATION(name, type, description, impact, scope) scope##_##name,
#include "inline.def"
#undef INLINE_OBSERVATION
};

https://github.com/dotnet/runtime/blob/486142a/src/coreclr/jit/inline.h#L165

src/coreclr/jit/inline.def#L96
INLINE_OBSERVATION(IS_FORCE_INLINE, bool, "aggressive inline attribute", INFORMATION, CALLEE)

https://github.com/dotnet/runtime/blob/486142a/src/coreclr/jit/inline.def#L96

CALLEE_IS_FORCE_INLINE

src/coreclr/jit/inline.cpp#L954-L971
//------------------------------------------------------------------------
// NoteAttempt: do bookkeeping for an inline attempt
//
// Arguments:
//    result -- InlineResult for successful inline candidate
void InlineStrategy::NoteAttempt(InlineResult* result)
{
    InlineObservation obs = result->GetObservation();
    if (obs == InlineObservation::CALLEE_BELOW_ALWAYS_INLINE_SIZE)
        m_AlwaysCandidateCount++;
    else if (obs == InlineObservation::CALLEE_IS_FORCE_INLINE)
        m_ForceCandidateCount++;
    else
        m_DiscretionaryCandidateCount++;
}

https://github.com/dotnet/runtime/blob/486142a/src/coreclr/jit/inline.cpp#L954-L971

src/coreclr/jit/inlinepolicy.cpp#L266-L289
//------------------------------------------------------------------------
// NoteBool: handle a boolean observation with non-fatal impact
//
// Arguments:
//    obs      - the current obsevation
//    value    - the value of the observation
void DefaultPolicy::NoteBool(InlineObservation obs, bool value)
{
    // Check the impact
    InlineImpact impact = InlGetImpact(obs);
    // Handle most information here
    bool isInformation = (impact == InlineImpact::INFORMATION);
    bool propagate     = !isInformation;

    if (isInformation)
    {
        switch (obs)
        {
            case InlineObservation::CALLEE_IS_FORCE_INLINE:
                // We may make the force-inline observation more than
                // once.  All observations should agree.
                assert(!m_IsForceInlineKnown || (m_IsForceInline == value));
                m_IsForceInline      = value;
                m_IsForceInlineKnown = true;
                break;
            ...
            default:
                // Ignore the remainder for now
                break;
        }
    }

    if (propagate)
        NoteInternal(obs);
}

https://github.com/dotnet/runtime/blob/486142a/src/coreclr/jit/inlinepolicy.cpp#L266-L289

で、我らがUnityでは、.NETのJITとは全く異なるランタイム、IL2CPPの上で生きています。IL2CPPはC#(からコンパイルされたIL)を完全にC++に変換してプラットフォームのネイティブとしてコンパイルさせます。巷ではIL2CPPでもAggressiveInliningは有効だといわれており、Unity公式のソースにも片っ端からつけられています。

今回は、それが実際どのようなC++に変換されているのかとともに、他のMethodImplOptionについても見ていきたいと思います。

C#コード

MethodImplOptionsを網羅的に書いてみました。属性をOn/Offして差分を見ていきましょう。

[asm:il2cpp_attribute] il2cpp_attribute.cs
public class il2cpp_attribute : MonoBehaviour
{
    [MethodImpl(MethodImplOptions.InternalCall)]
    public static extern void InternalCall();
    [MethodImpl(MethodImplOptions.Unmanaged)]
    public static extern void Unmanaged();
    [MethodImpl(MethodImplOptions.PreserveSig)]
    public static extern void PreserveSig();
    [MethodImpl(MethodImplOptions.ForwardRef)]
    public static extern void ForwardRef();


    [MethodImpl(MethodImplOptions.NoInlining)]
    public static void NoInlining() {}
    [MethodImpl(MethodImplOptions.Synchronized)]
    public static void Synchronized() {}
    [MethodImpl(MethodImplOptions.NoOptimization)]
    public static void NoOptimization() {}
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static void AggressiveInlining() {}
}
[asm:il2cpp_attribute_caller] il2cpp_attribute_caller.cs
public static class il2cpp_attribute_caller
{
    public static void CallInternalCall() => il2cpp_attribute.InternalCall();
    public static void CallUnmanaged() => il2cpp_attribute.Unmanaged();
    public static void CallPreserveSig() => il2cpp_attribute.PreserveSig();
    public static void CallForwardRef() => il2cpp_attribute.ForwardRef();
    public static void CallNoInlining() => il2cpp_attribute.NoInlining();
    public static void CallSynchronized() => il2cpp_attribute.Synchronized();
    public static void CallNoOptimization() => il2cpp_attribute.NoOptimization();
    public static void CallAggressiveInlining() => il2cpp_attribute.AggressiveInlining();
}

InternalCall

InternalCallをつけると、メソッドの中身はこのようなコードが追加されます。(名前につくハッシュは省略しています)

il2cpp_attribute.cpp
IL2CPP_EXTERN_C IL2CPP_METHOD_ATTR void il2cpp_attribute_InternalCall_m (const RuntimeMethod* method) 
{
	typedef void (*il2cpp_attribute_InternalCall_m_ftn) ();
	static il2cpp_attribute_InternalCall_m_ftn _il2cpp_icall_func;
	if (!_il2cpp_icall_func)
	_il2cpp_icall_func
         = (il2cpp_attribute_InternalCall_m_ftn)il2cpp_codegen_resolve_icall ("il2cpp_attribute::InternalCall()");
     _il2cpp_icall_func();
}

メソッド名をキーとして、il2cpp_codegen_resolve_icallを呼び出し、ランタイム側に実装された関数のテーブルを漁り、取得して呼び出します。

unityLibrary\src\main\Il2CppOutputProject\IL2CPP\libil2cpp\codegen\il2cpp-codegen.cpp
Il2CppMethodPointer il2cpp_codegen_resolve_icall(const char* name)
{
    Il2CppMethodPointer method = il2cpp::vm::InternalCalls::Resolve(name);
    if (!method)
        il2cpp::vm::Exception::Raise(il2cpp::vm::Exception::GetMissingMethodException(name));
    return method;
}
unityLibrary\src\main\Il2CppOutputProject\IL2CPP\libil2cpp\vm\InternalCalls.cpp
Il2CppMethodPointer InternalCalls::Resolve(const char* name)
{
    // Try to find the whole name first, then search using just type::method
    // if parameters were passed
    // ex: First, System.Foo::Bar(System.Int32)
    // Then, System.Foo::Bar
    ICallMap::iterator res = s_InternalCalls.find(name);

    if (res != s_InternalCalls.end())
        return res->second;

    std::string shortName(name);
    size_t index = shortName.find('(');

    if (index != std::string::npos)
    {
        shortName = shortName.substr(0, index);
        res = s_InternalCalls.find(shortName);

        if (res != s_InternalCalls.end())
            return res->second;
    }

    return NULL;
}

Unmanaged, PreserveSig, ForwardRef

Unmanagedは、C++/CLIのUnmanaged関数に、ForwardRefはIL SupportによってILで書いた関数にBindingするものです。PreserveSigはちょっとよくわからなかったのですが…(COM関連っぽい…?🤔)

これらは、IL2CPPには関係ないものなので、何も変化しませんでした。

il2cpp_attribute.cpp
IL2CPP_EXTERN_C IL2CPP_METHOD_ATTR void il2cpp_attribute_Unmanaged_m (const RuntimeMethod* method) 
{
	IL2CPP_RAISE_MANAGED_EXCEPTION(
         il2cpp_codegen_get_missing_method_exception(
             "The method 'System.Void il2cpp_attribute::Unmanaged()' has no implementation."), 
         NULL);
}
IL2CPP_EXTERN_C IL2CPP_METHOD_ATTR void il2cpp_attribute_PreserveSig_m (const RuntimeMethod* method) 
{
	IL2CPP_RAISE_MANAGED_EXCEPTION(
         il2cpp_codegen_get_missing_method_exception(
             "The method 'System.Void il2cpp_attribute::PreserveSig()' has no implementation."),
         NULL);
}
IL2CPP_EXTERN_C IL2CPP_METHOD_ATTR void il2cpp_attribute_ForwardRef_m (const RuntimeMethod* method) 
{
	IL2CPP_RAISE_MANAGED_EXCEPTION(
         il2cpp_codegen_get_missing_method_exception(
             "The method 'System.Void il2cpp_attribute::ForwardRef()' has no implementation."),
         NULL);
}

Synchronized

これは、そのメソッドの呼び出しにロックをかけるオプションです。つまり下のようなコードと同じふるまいをします。

SynchronizedMethod();
// ↓ 自動で
lock(hoge){ SynchronizedMethod(); }

これは上記のように何らかでロックを取った形にトランスパイルされる…かと思いきや、スルーされていました。

il2cpp_attribute.cpp
IL2CPP_EXTERN_C IL2CPP_METHOD_ATTR void il2cpp_attribute_Synchronized_m (const RuntimeMethod* method) 
{
    return;
}

このオプションは、BCLでもTextWriter.Synchronizedなどで使われているので、スレッドセーフだと思っていたメソッドが実はスレッドセーフじゃないということもあり得るので注意してください。

ちなみに、ConcurrentQueueなどのコンカレントコレクションは、MethodImplOptionではなく、lockで実装されているので安心して使えます。

NoOptimization

IL2CPP_DISABLE_OPTIMIZATIONSが付きました。

il2cpp_attribute.cpp
IL2CPP_DISABLE_OPTIMIZATIONS
IL2CPP_EXTERN_C IL2CPP_METHOD_ATTR void il2cpp_attribute_NoOptimization_m (const RuntimeMethod* method)
{
    return;
}

プラットフォームによっては、__pragma(optimize("", off))__attribute__ ((optnone))に展開されるようです。

unityLibrary\src\main\Il2CppOutputProject\IL2CPP\libil2cpp\codegen\il2cpp-codegen.h
#ifdef _MSC_VER
#define IL2CPP_DISABLE_OPTIMIZATIONS __pragma(optimize("", off))
#define IL2CPP_ENABLE_OPTIMIZATIONS __pragma(optimize("", on))
#elif IL2CPP_TARGET_LINUX || IL2CPP_TARGET_QNX
#define IL2CPP_DISABLE_OPTIMIZATIONS
#define IL2CPP_ENABLE_OPTIMIZATIONS
#else
#define IL2CPP_DISABLE_OPTIMIZATIONS __attribute__ ((optnone))
#define IL2CPP_ENABLE_OPTIMIZATIONS
#endif

NoInlining

IL2CPP_NO_INLINEという修飾が付きました。

il2cpp_attribute.cpp
IL2CPP_EXTERN_C IL2CPP_NO_INLINE IL2CPP_METHOD_ATTR void il2cpp_attribute_NoInlining_m (const RuntimeMethod* method)
{
    return;
}

IL2CPP_NO_INLINE__declspec(noinline)__attribute__ ((noinline))に展開されるようです。

unityLibrary\src\main\Il2CppOutputProject\IL2CPP\libil2cpp\il2cpp-config.h
#if IL2CPP_COMPILER_MSVC || defined(__ARMCC_VERSION)
    #define IL2CPP_NO_INLINE __declspec(noinline)
#else
    #define IL2CPP_NO_INLINE __attribute__ ((noinline))
#endif

AggressiveInlining

本命、AggressiveInliningです。これは2つの変化が見られました。

まず1つ目に、IL2CPP_ENABLE_OPTIMIZATIONSが付きました。これはNoOptimizationと対です。

il2cpp_attribute.cpp
IL2CPP_ENABLE_OPTIMIZATIONS
IL2CPP_EXTERN_C IL2CPP_METHOD_ATTR void il2cpp_attribute_AggressiveInlining_m (const RuntimeMethod* method) 
{
    return;
}

2つ目に、呼び出し側のcppファイルにコピーが生成されました。また、こちら側にはIL2CPP_MANAGED_FORCE_INLINEがつけられています。

il2cpp_attribute_caller.cpp
IL2CPP_MANAGED_FORCE_INLINE
IL2CPP_METHOD_ATTR void il2cpp_attribute_AggressiveInlining_m_inline (const RuntimeMethod* method) 
{
    return;
}

IL2CPP_MANAGED_FORCE_INLINEは、lnline__attribute__ ((always_inline))__forceinline に展開されるようです。

#if defined(__GNUC__) || defined(__SNC__) || defined(__clang__)
    #if defined(__GNUC__) && IL2CPP_GCC_VERSION < 100100
        #define IL2CPP_FORCE_INLINE inline
    #else
        #define IL2CPP_FORCE_INLINE inline __attribute__ ((always_inline))
    #endif
    #define IL2CPP_MANAGED_FORCE_INLINE IL2CPP_FORCE_INLINE
#elif defined(_MSC_VER)
    #define IL2CPP_FORCE_INLINE __forceinline
    #define IL2CPP_MANAGED_FORCE_INLINE inline
#else
    #define IL2CPP_FORCE_INLINE inline
    #define IL2CPP_MANAGED_FORCE_INLINE IL2CPP_FORCE_INLINE
#endif

しかし、inline系の属性がどうのというよりも、呼び出し側のcppファイルにコピーされていることに注目すべきです。C++では翻訳単位という概念が存在します。基本的には、includeなどのプリプロセッサを全展開した後のファイルが翻訳単位になります。

// それぞれの翻訳単位でコンパイルしオブジェクトファイルに
il2cpp_attribute.cpp → il2cpp_attribute.o
il2cpp_attribute_caller.cpp → il2cpp_attribute_caller.o

// オブジェクトファイルをリンク
il2cpp_attribute.o + il2cpp_attribute_caller.o

故に、関数の実装が別の翻訳単位に存在すると、オブジェクトファイルへのコンパイル時に中身がわからないのでインライン展開できません。そのため、C++では、インライン展開されてほしい関数は基本的にヘッダーに実装を書きます。

// myutil.h
inline void Functuin()
{
    printf("Hello World!");
}

// myutil.cpp
/* 実装無し */

// main.cpp
#include "myutil.h"
void main()
{
    Function();
}

比較用に普通に書くとこんな感じです。

// myutil.h
extern void Functuin();

// myutil.cpp
#include "myutil.h"
void Functuin()
{
    printf("Hello World!");
}

// main.cpp
#include "myutil.h"
void main()
{
    Function();
}

ところで、C++では、翻訳単位内に実装が複数個所あるとコンパイルエラーになってしまいます。なので、以下のように、インクルードしたいヘッダに別のヘッダがインクルードされていて…みたいなことがあると簡単に壊れてしまいます。

// myutil2.h
#include "myutil.h"
extern void Function2();

// main.cpp
#include "myutil.h"
#include "myutil2.h" /* 二重実装のコンパイルエラー */
void main()
{
    Function();
    Function2();
}

inline属性をつけると、実装が複数あっても、実装内容が同じであればコンパイルが通ります。

// myutil.h
inline void Functuin()
{
    printf("Hello World!");
}

// myutil2.h
#include "myutil.h"
inline void Functuin2() {}

// main.cpp
#include "myutil.h"
#include "myutil2.h"
void main()
{
    Function();
    Function2();
}

(私は面倒なので、main以外はいつも全部ヘッダにinlineで書いちゃってますw)

まとめ

まとめると、AggressiveInliningをつけると、inlineをつけるだけじゃなくて、インライン展開条件も満たされやすいコードをはいてくれますよということです。(再帰とかはさすがにどうにもなりませんが)

ちっちゃいコードにはバンバンAggressiveInliningをつけていきましょう!🔥

ライセンス表記

dotnet/runtime
The MIT License (MIT)

Copyright (c) .NET Foundation and Contributors

All rights reserved.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0