Ответ 1
Ну, это немного сложный ответ.
Здесь есть две вещи. (1) компилятор и (2) JIT.
Компилятор
Проще говоря, компилятор просто переводит ваш код С# на IL-код. Это довольно простой перевод для большинства случаев, и одна из основных идей .NET заключается в том, что каждая функция компилируется как автономный блок кода IL.
Итак, не ожидайте слишком многого от компилятора С# → IL.
JIT
Это... немного сложнее.
Компилятор JIT в основном переводит ваш IL-код на ассемблер. Компилятор JIT также содержит оптимизатор SSA. Однако существует ограничение по времени, потому что мы не хотим ждать слишком долго, пока наш код не начнет работать. В основном это означает, что JIT-компилятор не делает все супер классные вещи, которые сделают ваш код очень быстрым, просто потому, что это будет стоить слишком много времени.
Мы можем, конечно, просто поставить его на тест:) Убедитесь, что VS будет оптимизирован при запуске (параметры → отладчик → снять флажок подавлять [...] и только мой код), скомпилировать в режиме выпуска x64, поставить точку останова и посмотреть, что произойдет, когда вы переключитесь на просмотр ассемблера.
Но эй, какое удовольствие имеет только теория; пусть это будет проверено.:)
static bool Foo(Func<int, int, int> foo, int a, int b)
{
return foo(a, b) > 0; // put breakpoint on this line.
}
public static void Test()
{
int n = 2;
int m = 2;
if (Foo((a, b) => a + b, n, m))
{
Console.WriteLine("yeah");
}
}
Первое, что вы должны заметить, это то, что точка останова поражена. Это уже говорит о том, что метод не встроен; если бы это было так, вы бы не ударили по точке останова.
Далее, если вы посмотрите вывод ассемблера, вы увидите инструкции "вызова", используя адрес. Вот ваша функция. При ближайшем рассмотрении вы заметите, что он вызывает делегата.
Теперь, в основном это означает, что вызов не встроен и поэтому не оптимизирован для соответствия локальному (методу) контексту. Другими словами, не использовать делегатов и помещать материал в свой метод, вероятно, быстрее, чем использование делегатов.
С другой стороны, вызов довольно эффективен. В основном указатель функции просто передается и вызывается. Там нет vtable lookup, просто простой звонок. Это означает, что он, вероятно, бьет вызов члена (например, IL callvirt
). Тем не менее статические вызовы (IL call
) должны быть еще быстрее, поскольку они являются предсказуемым временем компиляции. Опять же, пусть тест, не так ли?
public static void Test()
{
ISummer summer = new Summer();
Stopwatch sw = Stopwatch.StartNew();
int n = 0;
for (int i = 0; i < 1000000000; ++i)
{
n = summer.Sum(n, i);
}
Console.WriteLine("Vtable call took {0} ms, result = {1}", sw.ElapsedMilliseconds, n);
Summer summer2 = new Summer();
sw = Stopwatch.StartNew();
n = 0;
for (int i = 0; i < 1000000000; ++i)
{
n = summer.Sum(n, i);
}
Console.WriteLine("Non-vtable call took {0} ms, result = {1}", sw.ElapsedMilliseconds, n);
Func<int, int, int> sumdel = (a, b) => a + b;
sw = Stopwatch.StartNew();
n = 0;
for (int i = 0; i < 1000000000; ++i)
{
n = sumdel(n, i);
}
Console.WriteLine("Delegate call took {0} ms, result = {1}", sw.ElapsedMilliseconds, n);
sw = Stopwatch.StartNew();
n = 0;
for (int i = 0; i < 1000000000; ++i)
{
n = Sum(n, i);
}
Console.WriteLine("Static call took {0} ms, result = {1}", sw.ElapsedMilliseconds, n);
}
Результаты:
Vtable call took 2714 ms, result = -1243309312
Non-vtable call took 2558 ms, result = -1243309312
Delegate call took 1904 ms, result = -1243309312
Static call took 324 ms, result = -1243309312
То, что интересно, на самом деле является последним результатом теста. Помните, что статические вызовы (IL call
) полностью детерминированы. Это означает, что это относительно простая вещь для оптимизации для компилятора. Если вы проверите выход ассемблера, вы обнаружите, что вызов Sum фактически встроен. Это имеет смысл. На самом деле, если вы проверите его, просто поместите код в этот метод так же быстро, как статический вызов.
Небольшое замечание о равных
Если вы измеряете производительность хэш-таблиц, что-то кажется подозрительным с моим объяснением. Похоже, что если IEquatable<T>
ускоряет работу.
Ну, это действительно так.:-) Контейнеры хеша используют IEquatable<T>
для вызова Equals
. Теперь, как мы все знаем, объекты все реализуют Equals(object o)
. Таким образом, контейнеры могут вызывать Equals(object)
или Equals(T)
. Производительность самого вызова одинакова.
Однако, если вы также реализуете IEquatable<T>
, реализация обычно выглядит так:
bool Equals(object o)
{
var obj = o as MyType;
return obj != null && this.Equals(obj);
}
Кроме того, если MyType
является структурой, среда выполнения также должна применять бокс и распаковку. Если бы он просто вызывал IEquatable<T>
, ни один из этих шагов не был бы необходим. Таким образом, хотя он выглядит медленнее, это не имеет никакого отношения к самому вызову.
Ваши вопросы
Будет ли какое-либо преимущество (производительность) перемещать оценку ListOfDates.Max() из предложения Where в этом случае или будет 1. компилятор или 2. JIT оптимизировать это?
Да, будет преимущество. Компилятор /JIT не будет оптимизировать его.
Я считаю, что С# будет делать постоянную фальцовку во время компиляции, и можно утверждать, что ListOfDates.Max() не может быть известен во время компиляции, если только ListOfDates не является константой.
Собственно, если вы измените статический вызов на n = 2 + Sum(n, 2)
, вы заметите, что выход ассемблера будет содержать 4
. Это доказывает, что оптимизатор JIT делает постоянную фальцовку. (Что совершенно очевидно, если вы знаете, как работают оптимизаторы SSA... const fold и упрощение называются несколько раз).
Сам указатель функции не оптимизирован. Возможно, это будет в будущем.
Возможно, существует другая оптимизация компилятора (или JIT), которая гарантирует, что это оценивается только один раз?
Что касается "другого компилятора", если вы хотите добавить "другой язык", вы можете использовать С++. В С++ эти виды вызовов иногда оптимизируются.
Более интересно, Clang основан на LLVM, и есть несколько компиляторов С# для LLVM. Я считаю, что Mono имеет возможность оптимизировать LLVM, а CoreCLR работает над LLILC. Хотя я не тестировал это, LLVM может определенно выполнять такие виды оптимизации.