Низкая производительность DynamicMethod

Я попытался улучшить производительность некоторого фрагмента кода в своем проекте, сгенерировав IL специально для этой задачи.

В настоящее время эта задача выполняется путем выполнения цикла for над элементами массива и запуска различных методов через интерфейс. Я хотел заменить его на IL, который специально выполняет эту задачу без каких-либо вызовов виртуального/интерфейса (непосредственно выполняя необходимые операции).

По какой-то причине производительность этого DynamicMethod во время выполнения намного ниже, чем производительность исходного кода, выполняющего интерфейсные вызовы для каждого элемента. Единственная причина, по которой я вижу, заключается в том, что мой DynamicMethod довольно большой (несколько инструкций на элемент массива).

Я думал, что это может быть первый медленный вызов из-за JIT, но это не так. Все вызовы медленнее. Кто-нибудь сталкивался с подобным?

изменить

Люди здесь запрашивают код ... исходный код довольно большой, но вот уменьшенная версия (это код автоматической дифференциации для вычисления градиента функции с использованием обратного режима AD). Все элементы в моем массиве наследуют следующий класс

abstract class Element
{
    public double Value
    public double Adjoint
    public abstract void Accept(IVisitor visitor)
}

У меня есть два класса, которые происходят от element. Для простоты я определю только следующие два

class Sum : Element
{
    public int IndexOfLeft;   // the index in the array of the first operand
    public int IndexOfRight;  // the index in the array of the second operand
    public abstract void Accept(IVisitor visitor) { visitor.Visit(this); }
}

class Product : Element
{
    public int IndexOfLeft;   // the index in the array of the first operand 
    public int IndexOfRight;  // the index in the array of second first operand 
    public abstract void Accept(IVisitor visitor) { visitor.Visit(this); }
}

Вот реализация посетителя:

class Visitor : IVisitor
{
    private Element[] array;

    public Visitor(Element[] array) { this.array = array; }

    public void Visit(Product product)
    {
        var left = array[product.IndexOfLeft].Value;
        var right = array[product.IndexOfRight].Value;

        // here we update product.Value and product.Adjoint according to some mathematical formulas involving left & right
    } 

    public void Visit(Sum sum)
    {
        var left = array[sum.IndexOfLeft].Value;
        var right = array[sum.IndexOfRight].Value;

        // here we update sum.Value and product.Adjoint according to some mathematical formulas involving left & right
    }       
}

Мой исходный код выглядит так:

void Compute(Element[] array)
{
    var visitor = new Visitor(array);
    for(int i = 0; i < array.Length; ++i)
        array[i].Accept(visitor);
}

Мой новый код пытается сделать что-то вроде этого

void GenerateIL(Element[] array, ILGenerator ilGenerator)
{
    for(int i = 0; i < array.Length; ++i)
    {
        // for each element we emit calls that push "array[i]" and "array" 
        // to the stack, treating "i" as constant,
        // and emit a call to a method similar to Visit in the above visitor that 
        // performs a computation similar to Visitor.Visit.
    }
}

Затем я вызываю сгенерированный код... и он выполняется медленнее, чем двойная отправка, которую я получаю с шаблоном посетителя при вызове Compute(array);


person Alex Shtof    schedule 25.09.2012    source источник
comment
Судя по всему, вы пишете компиляторы не лучше Microsoft. Я бы просто сказал, что IL не предназначен для создания программистами; вам лучше просто написать хороший код C# и оставить IL в покое. Если вам действительно нужно вносить изменения на таком низком уровне (и вы не выполняете преждевременную оптимизацию), вам, вероятно, следует кодировать на C/C++.   -  person Servy    schedule 25.09.2012
comment
Я все еще хотел бы знать, почему... код был сгенерирован путем копирования из существующего кода, сгенерированного компилятором Microsoft C # с использованием Reflector.   -  person Alex Shtof    schedule 26.09.2012
comment
Не могли бы вы прокомментировать, что вы имеете в виду, довольно большой (несколько инструкций на элемент массива)? Почему размер метода зависит от размера массива? (Примечание: может быть хорошей идеей показать код IL и/или код, используемый для построения IL... постарайтесь сделать образец небольшим, чтобы люди действительно могли видеть, о чем вы говорите).   -  person Alexei Levenkov    schedule 26.09.2012
comment
По сути, вы говорите: «У меня есть два фрагмента кода, один медленнее другого, можете ли вы объяснить, почему это так?» Ответ: «Нет, мы не можем, если вы действительно не покажете нам свой код!»   -  person svick    schedule 26.09.2012
comment
Я добавил пример кода, как вы просили. Надеюсь, поможет.   -  person Alex Shtof    schedule 27.09.2012
comment
Такой подход вполне разумен. Я совершенно не согласен с Серви. Это хорошо известный метод оптимизации и причина, по которой многие фреймворки (ORM, ...) генерируют IL.   -  person usr    schedule 28.09.2012
comment
Пожалуйста, опубликуйте код своего поколения IL и того, что вы делаете с заполненным ILGenerator. Из комментария к последнему опубликованному вами методу видно, что вы пытаетесь повысить производительность за счет развертывания цикла. Я не верю, что это поможет вам в этом случае, если только вы не кэшируете методы по размеру массива и намерению. Генерация и последующий вызов кода для развертывания цикла (как правило) намного дороже, чем простой вызов кода для однократного прокрутки элементов в наборе.   -  person mlorbetske    schedule 28.09.2012
comment
Там нет ничего интересного. Он вызывает DynamicMethod.CreateDelegate, а затем вызывает делегат много (несколько тысяч) раз. Я не жалуюсь на производительность генерации кода. Я жалуюсь на производительность сгенерированного кода. Я пытаюсь предотвратить вызовы методов виртуального/интерфейса, напрямую вызывая правильный метод для каждого элемента.   -  person Alex Shtof    schedule 28.09.2012
comment
Вместо IL, почему бы вам не сгенерировать исходный код и позволить компилятору скомпилировать его и предоставить вам вновь сгенерированную dll, которую можно использовать, не беспокоясь о компиляции. Вот как работает asp.net, все файлы aspx компилируются в dll и вызываются IIS.   -  person Akash Kava    schedule 28.09.2012
comment
Вы уверены, что виртуальные вызовы интерфейса являются вашим узким местом, и вы не можете получить больше улучшений производительности в другом месте?   -  person Kirill Bestemyanov    schedule 28.09.2012
comment
Взгляните на эти ссылки msdn.microsoft.com/en-us/magazine/ cc163759.aspx codeproject.com /Статьи/10951/   -  person mlorbetske    schedule 29.09.2012
comment
Можете ли вы привести короткий, но полный пример, демонстрирующий разницу в скорости? Это облегчило бы диагностику намного.   -  person Jon Skeet    schedule 29.09.2012
comment
просто поместите Datetime start = Datetime.Now и Datetime end = Datetime.Now и Console.writeline(end-start).totalmiliseconds вокруг каждого подозрительного места, что приведет к снижению производительности и циклу 1000 раз. Я не нахожу ничего, что могло бы снизить производительность, кроме вашего алгоритма манипулирования данными.   -  person Larry    schedule 02.10.2012
comment
@AkashKava, я не хочу генерировать код заранее, потому что информация, необходимая для генерации кода, зависит от ввода пользователя из пользовательского интерфейса.   -  person Alex Shtof    schedule 03.10.2012
comment
@KirillBestemyanov, да, уверен. Когда я делаю это без вызовов виртуальных методов (и тогда у меня есть только один тип элемента) и когда я делаю это с виртуальными вызовами (все еще имея только один тип элемента), я вижу большую (x6) разницу в производительности.   -  person Alex Shtof    schedule 03.10.2012
comment
@Alex: Похоже, что содержимое GenerateIL поможет убедиться, что ваш сгенерированный IL правильный. Вы можете использовать pastebin.com, если он слишком велик для этого места. Вы говорите, что это просто, но, учитывая, что вы вручную разворачиваете цикл с более чем тысячей элементов, небольшое изменение в IL может иметь огромное значение.   -  person Guvante    schedule 05.10.2012


Ответы (4)


Если я правильно понял, вы пытаетесь устранить накладные расходы на вызов виртуального метода, испуская сам код и вызывая метод напрямую. Например, вместо вызова тысяч виртуальных функций вы хотите вызвать одну виртуальную функцию.

Но вы хотите, чтобы разные объекты имели одинаковый интерфейс. Добиться этого можно только виртуальными звонками. Либо реализация интерфейса, либо использование делегатов, либо испускание кода. Да, даже если вы создаете код, вам нужен какой-то интерфейс для вызова этого метода, который может вызывать делегат или приводить его к предопределенным делегатам func/action.

Если вы хотите иметь какой-то эффективный способ создания кода, я предлагаю использовать «LambdaExpression.CompileToMethod». Этот метод использует построитель методов, и я предполагаю, что он у вас уже есть. Вы можете увидеть множество примеров в Интернете. Но тем не менее, это также приведет к виртуальному звонку.

В результате, если вы хотите иметь одинаковый интерфейс во многих объектах, у вас не может быть невиртуальных вызовов, если только вы не поместите свои объекты в разные корзины в зависимости от их типов. Что против полиморфизма.

person zahir    schedule 30.09.2012

Если вы действительно заинтересованы в супероптимизации своего кода, вам нужно изучить IL!

Взгляните на коды IL OP по следующей ссылке...

http://msdn.microsoft.com/en-us/library/system.reflection.emit.opcodes(v=vs.95).aspx

также используйте ILDasm, чтобы взглянуть на код, который вы создали из своих методов...

Хотя я подозреваю, что вы не сможете сильно оптимизировать IL, и было бы гораздо лучше написать его на C++ и вызвать неуправляемый код...

просто мысль для вас...

Удачи Мэтью

person Matthew Zielonka.co.uk    schedule 04.10.2012

Мне любопытно, почему название является динамическим методом. Когда вы создаете файл IL. Вы имеете в виду динамическую генерацию IL, а затем статическое выполнение. или вы также создаете IL, который использует IL-эквивалент динамического ключевого слова С#?

Динамический (время выполнения) IL

Я предполагаю, что код обрабатывается только один раз. И вы это проверили.

Использование массива, а не дженериков в представленном образце добавляет загадочности. Проблема заключается в сгенерированном IL, а не в коде, генерирующем IL. Но если вы использовали ARRAYs в сгенерированном IL, вы будете использовать box unbox. Дорогие STACK/HEAP и обратные операции.

Генерируете ли вы IL код, который использует BOX и UNBOX. ИЛ операции? Я бы начал прямо с этого.

Инициализация коллекции — это следующее, на что стоит обратить внимание.

Еще несколько быстрых соображений: пометка БОЛЬШИХ разделов кода в надежде сэкономить накладные расходы на вызов метода может негативно сказаться на времени JIT. Поскольку компилятор должен иметь дело со всем методом/членом. Если у вас есть небольшие методы, они компилируются при необходимости. Но вы сказали, что это не проблема JIT.

Эти БОЛЬШИЕ методы могут иметь большие операции стека?

Любые большие объекты Value внутри методов, которые часто вызываются? например, объекты STRUCT размером > 64 байта? Выделение и уничтожение стека каждый раз.

Что вам говорит профилировщик производительности RedGate? http://www.red-gate.com/products/dotnet-development/ants-performance-profiler/?utm_source=google&utm_medium=cpc&utm_content=unmet_need&utm_campaign=antsperformanceprofiler&gclid=CIXamdiA6bICFQYcpQodDA0Akw

Кстати, я новичок в IL. Просто подкинул пару идей.

удачи.

person phil soady    schedule 05.10.2012

Вы пытались заставить JIT использовать более быструю память, заключив цикл в блок try-catch? Это также имеет то преимущество, что удаляет условие выхода и, таким образом, экономит вам немного IL.

try
{
    for (int i= 0; ; i++)
    {
        var visitor = new Visitor(array);
        for(int i = 0;; ++i)
            array[i].Accept(visitor);
    }
}
catch (IndexOutOfRangeException)
{ }

Это выглядит ужасно, но в нем используется причуда выделения памяти JIT, которая может помочь решить вашу проблему с производительностью IL.

Дополнительную информацию об этом см. в разделе Оптимизация цикла for.

person biofractal    schedule 02.10.2012