Как стать автором
Обновить

Комментарии 48

НЛО прилетело и опубликовало эту надпись здесь
Не зря приводится JMH — чтобы начать очень и очень рекомендуется ознакомиться с jmh примерами.
Да, есть общие правила, применимые почти ко всем микробенчмаркам. Хотя даже если следовать им, не всегда удаётся измерить правильно. Поэтому любой бенчмарк без должного анализа результатов не имеет смысла.

Вот знаменитый вопрос на StackOverflow. Или ещё полезный пост с примерами.

По моим наблюдениям самые частые ошибки такие.

  • Один большой метод, в котором замеряют все алгоритмы. Это в корне неверно. Единица JIT компиляции — метод. Чем длиннее метод, тем сложнее его оптимизировать. Скорее всего, код будет адаптирован лишь под один из алгоритмов, по которому успела собраться run-time статистика. Разные алгоритмы надо запускать в отдельных JVM.
     
  • Весь бенчмарк — один длинный цикл в main(). Таким образом, вместо полноценно скомпилированного метода тестируют OSR заглушку. Один прогон бенчмарка следует поместить в отдельный метод, который запускается несколько раз.
     
  • Замеры надо проводить в устойчивом состоянии, когда все классы уже загружены, все компиляции-рекомпиляции уже прошли и т. д. Нет единого «золотого» числа итераций, после которого приложение заведомо будет работать с максимальной скоростью. Для одних тестов это пара секунд, для других — несколько минут.
     
  • «Прогревать» следует ровно тот код, который измеряется. Иначе Profile Pollution может всё испортить.
     
  • Бенчмарк должен иметь эффект, чтобы JIT не выкинул код, который что-то вычисляет, но нигде результаты вычислений не использует.
     
  • Самые честные замеры — на свежей 64-битной JDK в режиме Tiered или C2. Показатели 32-битной JVM и 64-битной, «клиентского» компилятора и «серверного» могут отличаться на порядок.
     
  • Убедитесь, что памяти достаточно, и что GC не оказывает влияния на производительность.
     
  • Минимизируйте сторонние эффекты. Избегайте лишних println-ов, nanoTime-ов и прочих операций, не имеющих отношения к измеряемому коду.
     
Из статьи можно сделать только вывод — давайте не будем измерять производительность в тестах время выполнения которого мало.
Это недалеко от истины. Если программа запускается раз в сутки, то, как правило, глубоко пофиг, сколько она выполняется — 300 миллисекунд или 600.
Оно это «глубоко пофиг» имеет свойство накапливаться. Впрочем, подход «вот когда накопится, тогда и будем этим заниматься» тоже имеет право на жизнь.
НЛО прилетело и опубликовало эту надпись здесь

Так этим и занимается JIT прямо на боевой системе: что часто вызывается, то оптимизирует (компилируя более продвинутым компилятором C2). Что редко вызывается, то не сильно оптимизирует (компилируя более быстрым компилятором C1).

НЛО прилетело и опубликовало эту надпись здесь
Если разница будет существенной (скажем, на порядок) — то скорее всего и на пользовательской машине все будет примерно также.

Ждал про то как меняется производительность от контекста, и что с этим делать.

Перед глазами сразу встала картина конца девяностых.
Мой командир (я учился в полувоенном вузе) попросил меня сделать ему программу для автоматического составления графика нарядов. Но непременно в Экселе.
На тот момент я ничего лучше не придумал, чем сделать это всё на рекурсивных ссылках. Работало медленно, но в целом скорость была приемлима.
Через месяц я таки узнал что есть такое понятие как VBA и решил переписать всё по человечески, процедурой.
Полдня работы, и новая версия готова.
отработала она у меня на порядок, если не на два быстрее.
Приношу к командиру, а она у него работает медленнее чем прошлая версия.
Не помню в чем конкретно был момент, давно это было, да и говнокода своего уже не вспомнить. Но помню что разгадка была в том, что у одного из компов был полноценный сопроцессор, вроде даже ММХ, а второй был стареньким клоном 486, но зато памяти было аж 32мб, в отличии от 16мб на другом.
Для меня это тогда было настоящим откровением. Я примерно догадывался что профиль железа может несколько исказить пропорции в производительности, но чтобы вот так, прямо противоположным образом, то не думал…

Не могу себе представить 16 мб памяти, сейчас обычно используют как минимум 16 гб)

Сначала мы пишем на Ассемблере, и говорим что сложно писать большие сложные программы, потом придумываем С.
Потом мы пишем на СИ, а потом говорим что тяжело без ООП, Придумываем С++.
Потом мы пишем на С++ и понимаем что указатели зло. Потом придумываем Java и С#, потом говорим что они очень медленные ))
Каждый язык что-то чинит )) А перфоманс лишь последствие того что мы решили починить в других языках ))


Недавно посмотрел https://youtu.be/_79KfX-3sQc?t=70 очень позновательно

Да всё просто — чем выше уровень языка — тем медленнее конечная программа, и тем быстрее разработка. Заказчику не важно, насколько медленно работает программа (если её покупают), зато ему важно ускорить разработку, чтобы меньше платить за человеко-часы. Вот и получается, от нового железа ждут, что оно будет работать быстрее, а от нового софта, почему-то, что он будет работать медленнее. Если когда-нибудь закон Мура перестанет таки действовать — нас догонит этот эффект, и это будет очень больно. Но до тех маркетологам удаётся провернуть трюк «Купите новый компьютер — не будет тормозить».
НЛО прилетело и опубликовало эту надпись здесь
Загрузить классы самого Stream API (их там немало)

что-то сомневаюсь, что он прям весь Stream API загружает.
Но согласен, что загрузка классов в Java может занимать существенное время.

Интересный вопрос и это легко проверить. Однократный вызов метода test() на свежезапущенной JVM загружает 206 классов. Из них:


  • 40 шт — java.lang.invoke (всё что связано с поддержкой MethodHandle и генерацией ламбда-форм, плюс LambdaMetafactory)
  • 14 шт — библиотека ASM (генерация байткода, благодаря которой работает LambdaMetafactory)
  • 27 шт — классы/интерфейсы java.util.stream и сплитераторы
  • 4 шт — интерфейсы из java.util.function
  • 44 шт — прочие классы стандартного рантайма (rt.jar), многие вроде ArrayList всё равно бы загрузились в реальном приложении позже, но измерили их загрузку мы именно сейчас.
  • 76 шт — сгенерированные классы для лямбда-форм (если кому интересно, зачем это, смотрите Владимира Иванова)
  • 2 шт — рантайм-представления двух лямбд из теста.

Как оказалось, лямбд внутри Stream API при этой операции не генерируется ни одной, тут у меня было ложное представление! Но так или иначе классов немало.


  • Edit: забыл написать, проверял с помощью java -verbose:class.
<ancient_iron>Core i7? Не самом быстром? Автор явно не видел моего Pentium Dual-Core E5500</ancient_iron>

Я имел в виду, на не самом быстром из существующих Core i7. Почему не видел? Не так давно продал как раз такой, он мне много лет служил верой и правдой :-)

А мой мне до сих пор служит. Даже ядро не раз собрал :)
Тут есть философский вопрос насчёт прогрева.
Ну то есть запустить 1 прогон, чтобы точно загрузились классы — с этим никто не спорит.
Но вот давать jit компилятору набрать статистику и оптимизировать код — это уже большой вопрос.
Во-первых, тестовые данные это обычно примитивная лажа с потолка. Ну, условно, в примере из статьи берутся числа от 1 до 100000, причём подряд, но в реальных задачах будут те же 100000 чисел, но каких-нибудь реальных. И статистика и, соответственно, оптимизации будут совсем другими.
Второе — в программах на Java довольно редко один и тот же алгоритм гоняется в цикле тысячи раз. Такие алгоритмы это обычно какой-нибудь матан, и его чаще всего не на Java считают.
Соответственно в реальной программе тестируемый метод будет вызываться, вероятно, относительно редко, единицы-десятки раз за прогон. Т.е. на практике не будет статистики, не будет мегамахровой оптимизации, и как раз важно время второго-третьего прогона, а не миллионного.
Фокус в том, что в реальных программах ничего прогревать не нужно. Это делается само собой, причём статистика собирается на полезной нагрузке. Приложения могут работать часами, днями, месяцами… А, вот, в микробенчмарках такое поведение воспроизвести сложно. Хочется померить маленький кусок кода и очень быстро. Отсюда и приёмчики с искусственным прогревом и т. п.

в реальной программе тестируемый метод будет вызываться, вероятно, относительно редко
Редко вызываемый код почти никогда не будет проблемой. Ну, отработает он за 10мс. Ну, за 100… Человек разницы даже не заметит. А если код работает долго — значит, там, есть циклы, есть повторения… Те самые тысячи и миллионы прогонов по одним и тем же инструкциям, которые JIT и призван оптимизировать.
Редко вызываемый код почти никогда не будет проблемой

Ну конечно.
Java — это, обычно, махровый энтерпрайз.
То есть имеются тысячи планктона, которые чего-то там в систему вводят.
И есть биг-босс, который раз в неделю или раз в квартал смотрит отчёт.
И вот ему этот самый отчёт нужно предоставить очень быстро. Иначе он сочтёт, что ваша программа «тормозит» и вообще «не очень».
Трюк в том, что именно этот биг-босс принимает решение о финансировании. А не рядовые клерки.
Ну и? Ваши действия?

У вас в голове неправильный масштаб времени происходящего. Смотрите: если метод выполняется один раз и не имеет блокировок (будь то mutex или i/o, или другой синхронный системный вызов), он либо выполняется не больше миллисекунды (даже в интерпретируемом режиме), либо вызывает другие методы в цикле, либо содержит большой цикл, который не вызывает других методов. В первом случае даже бигбоссу плевать. Во втором уже через несколько итераций цикла все вызываемые методы будут JIT-компилированы и код уже разгонится до весьма приличной скорости (а потом дооптимизируется до максимальной). В третьем (который очень редкий в реальной жизни) спасает механизм OSR. В любом случае меньше чем за секунду даже гигантский объём кода будет весь приведён в боевое состояние, поэтому бигбоссу плевать на JIT. Тормоза в Java кроются совсем в других местах (как раз i/o, избыточная синхронизация, сборка мусора, если программа чрезмерно гадит, и т. д.)

наверное стоит начать махровый энтерпрайз с высокочастотного трейдинга

потом можно продолжить поисковым движком elasticsearch (напомните мне схожий по возможностям продукт не на java, вроде sphinx что-то умеет, правда количество упоминаний и инсталяций в разы меньше)

дальше продолжаем быстрой распределенной очередью, чтобы заменить kafka (zeromq это конструктор, а не готовая распределенная персистентная очередь)

так как нам нужно распределенно считать, то следом в очередь spark, storm (даже у последователя heronосновной объем кода это java), gearpump

можно еще упомянуть hbase (бедные Airbnb и Xiaomi не знаю, что нельзя его было использовать) или его сородича accumulo (писалась по заказу АНБ с требованием жестких ACL вплоть до отдельной ячейки)

вот так сидят все и формочки клепают

Ещё в копилку классных проектов на Java — Apache Cassandra

Лучший бенчмарк — это ваша программа.

1. Возьмите самое сердце вашего кода — самый важный и тормозной кусок.
2. Перепишите его на другом языке. Для начала, буквально перепишите. Без оптимизаций.
3. Померяйте производительность.

Если в переписанном куске уже стало намного быстрее, значит однозначно меняйте язык для этого куска. Если нет, то смотрите внимательно, что можно оптимизировать. Например, если переписывать с Java на C, то можно применить векторизацию… В общем, думайте.

Но Python почти всегда намного медленнее, чем Java. И это — факт. Трудно придумать тест, на котором будет наоборот.
Одна из самых больших глупостей — менять систему на другую без диагностики проблем.
Речь идет не о проблемах, а о выборе языка под задачу.
Зачем вообще тогда это делать, если нет проблемы?
А кто сказал, что без диагностики?

Искусственные бенчмарки почти никогда не дают верного результата, потому что меряют не то, что нужно конкретно вам. Единственный правильный бенчмарк — это профилирование вашей собственной программы. Написали — тормозит — запустили профилировщик — нашли узкие места — поправили. Обычно «поправили» — и есть смена языка. Например, с Java на C+JNI.

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

Смена языка на основе левых бенчмарков — вот это реально глупость.

Вообще-то «запустили профилировщик» — это тоже не так просто. В зависимости от ситуации профилировщик легко может выдать мусор вместо реальных данных. Программа под профилировщиком ведёт себя не так как программа в продакшне.


Обычно «поправили» — и есть смена языка.

Мой опыт говорит, что большинство проблем с производительностью на Java решается оптимизацией Java-кода, а не впиливанием C+JNI. Очень редко впиливать C+JNI оправданно. У вас другой опыт? Можете рассказать пару историй успеха, когда вам пришось впилить C+JNI?

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

Java эффективна в управлении, но не годна для математики из-за своей «тяжелой» концепции управления памятью. Но так как отлаживать код на ней легко и приятно, часто удобно сперва написать весь алгоритм на Java, затем полюбоваться, как тормозит «внутренний цикл», запустить VisualVM, обнаружить самые «узкие» места и переписать их на C++.
Вот отличный канонический пример на тему «Java тормозит».

Чувак пишет бенчмарк, замеряющий скорость работы с memory-mapped файлом на Java и на C.
Запускает… Программа на Java работает в 500 раз медленнее! Какой вывод? Дрянь эта ваша Java!

Но надо отдать должное автору, он не стал сразу ругать Java, а обратился на StackOverflow с вопросом «почему?» Можно было пойти дальше и проанализировать самому, хотя бы с помощью профайлера. И тут становится понятно, что Java-то не виновата. Это программист дёргает на каждый байтик file.size(). А если переписать тест грамотно, то окажется, что разница в скорости вообще копеечная.
Я люблю Java за то, что она — самый производительный из высокоуровневых C++-образных языков.

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

Сравним:

public class ThousandGB {
	public static void main(String... args) {
		
		int[] arr;
		
		for (int i = 0; i < 100000; i++) {
			arr = new int[1024 * 1024];
		}
	
	}
}


и аналогичный на C++
int main() {

	int* arr;
	
	for (int i = 0; i < 100000; i++) {
		arr = new int[1024 * 1024];
		delete [] arr;
	}

}


Код специально тривиален — всё, что я хочу показать — это оверхед по выделению памяти. Мы сто тысяч раз раз выделяем один мегабайт. И вот вам результат (запускал несколько раз подряд, всё «разогрето»):

$ time java ThousandGB

real    1m1.992s
user    0m0.015s
sys     0m0.030s


$ time ./ThousandGB.exe

real    0m1.320s
user    0m0.000s
sys     0m0.046s


Разница порядка 40-50 раз.

Вы можете возразить, что я не использую «нативные буферы», но они, по сути, являются той же самой JNI-оберткой вокруг массивов.

Вывод №1: как только в Java надо ворочать большими блоками данных, она начинает лагать.
Вывод №2: скорость Java-кода варьируется от ~1/2 скорости нативного и до ~1/50, в зависимости от задачи. Если оба коэффициента вам кажутся приемлимой платой за ее удобство и безопасность, используйте Java и радуйтесь. У Python, кажется (когда-то мерял), этот коэффициент начинается от 1/100…
Well, well

А теперь поясните, что я делаю не так:

код на C++:
int main() {

    int* arr;
    int sum = 0;
    for (int i = 0; i < 10000; i++) {
        arr = new int[1024 * 1024];
        for(int j = 0; j < 1024 * 1024; j++)
                sum += arr[j];
        delete [] arr;
    }
    return sum;

}


и такой же, но на java:
public class A {
public static void main(String... args) {

        final long start = System.nanoTime();

        int sum = 0;
        for (int i = 0; i < 10_000; i++) {
            int[] arr = new int[1024 * 1024];
            for (int j = 0; j < arr.length; j++) {
                sum += arr[j];
            }
        }

        final long end = System.nanoTime();
        System.out.println(sum + " : " + (end - start) / 1e3 / 1000 + " ms");
    }
}


$ g++ a.cpp
$ time ./a.out

real    0m27.037s
user    0m27.003s
sys     0m0.009s

$ javac A.java
$ time java A
0 : 8429.422174000001 ms

real    0m8.522s
user    0m7.944s
sys     0m1.017s

Это всё было на 10 тыс циклов, а вот то же самое, но как у вас на 100 тысячах:

$ time java A
0 : 86501.16352799999 ms

real    1m26.593s
user    1m28.326s
sys     0m1.291s

$ time ./a.out

real    4m29.753s
user    4m29.496s
sys     0m0.010s
Спасибо за красивый пример. В очередной раз убедился, что нельзя никогда создавать искусственные бенчмарки, а мерить производительность надо только на реальном коде. Потому что причин, по которым код может работать быстрее/медленнее — миллион. Понимать их можно лучше или хуже, но все подобные примеры — голая софистика.

Мне другое интересно — почему внутренний цикл с арифметикой на Java быстрее? Я ниже подробнее написал — прочтите, пожалуйста: https://habrahabr.ru/post/307268/#comment_9764236
Спасибо за этот пример — добавлю его в копилочку классических ошибок бенчмаркинга :)
Я уже писал, что большинство заблуждений про производительность Java происходят из-за плохого кода и неумения анализировать. Давайте же вместе проанализируем.

Смотрите: ваш тест на C++ выделяет 100 тыс. раз по 4 мегабайта, так? (Размер int обычно 4 байта). Итого около 400 GB. И всё это за одну секунду. При этом пропускная способность самой современной топовой DDR4 памяти составляет всего 25 GB/s. Опа! Ваш тест в 16 раз опередил время!

Подключив здравый смысл, становится очевидно, что ваш тест замеряет что-то не то. (А что именно, кстати?) Более или менее корректный тест уже написал Владимир выше. И из него следуют ровно обратное. Действительно, выделение памяти — это как раз сильная сторона Java. Я на StackOverflow рассказывал, как работает выделение в Java — в большинстве случаев это всего 10-15 тактов CPU. Даже самым крутым сишным аллокаторам (tcmalloc, jemalloc) далеко в этом смысле до JVM.
Мн-да. Очень странно было с моей стороны доказывать бессмысленность искуственно созданных бенчмарков с помощью создания СВОЕГО искусственного бенчмарка.

Спасибо за интересный пример товарищу сверху. Он в очередной раз меня убедил в том, что единственный способ понять слабые и сильные стороны производительности — это использовать профилировщик. Все остальные методы — полезное упражнение на знание внутреннего устройства системы, но не более того.

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

Куда больше меня напрягает то, что последующие манипуляции этой памятью в Java быстрее. Этого я реально не ожидал.

То есть, рассмотрим вот такой пример:

int main() {

    int* arr;
    int sum = 0;
    for (int i = 0; i < 10000; i++) {
		arr = new int[1024 * 1024];
        for(int j = 0; j < 1024 * 1024; j++) {
			arr[j] = j * j;
			sum += arr[j];
		}
		delete [] arr;
    }
	return sum;

}


public class ThousandGB2 {
public static void main(String... args) {
        int sum = 0;
        for (int i = 0; i < 10000; i++) {
			int[] arr = new int[1024 * 1024];
            for (int j = 0; j < 1024 * 1024; j++) {
                arr[j] = j * j;
				sum += arr[j];
            }
        }
    }
}


Результаты:

$ g++ -o ThousandGB2 ThousandGB2.cpp -O3 && time ThousandGB2

real    0m21.006s
user    0m0.000s
sys     0m0.046s

$ javac ThousandGB2.java && time java -Xmx300m ThousandGB2

real    0m15.303s
user    0m0.000s
sys     0m0.047s


А теперь заменим предел ВНУТРЕННЕГО цикла:

int main() {

    int* arr;
    int sum = 0;
    for (int i = 0; i < 10000; i++) {
		arr = new int[1024 * 1024];
        for(int j = 0; j < 1024 * 100; j++) {
			arr[j] = j * j;
			sum += arr[j];
		}
		delete [] arr;
    }
	return sum;

}


public class ThousandGB2 {
public static void main(String... args) {
        int sum = 0;
        for (int i = 0; i < 10000; i++) {
			int[] arr = new int[1024 * 1024];
            for (int j = 0; j < 1024 * 100; j++) {
                arr[j] = j * j;
				sum += arr[j];
            }
        }
    }
}


Новые результаты:

$ g++ -o ThousandGB2 ThousandGB2.cpp -O3 && time ThousandGB2

real    0m2.120s
user    0m0.015s
sys     0m0.015s

$ javac ThousandGB2.java && time java -Xmx300m ThousandGB2

real    0m5.848s
user    0m0.000s
sys     0m0.031s


То есть я наблюдаю следующую картину: количество выделений памяти, предположительно, не изменилось (я прав?). Но внутренний цикл с арифметикой на C++ явно медленнее, чем на Java. То есть компилятор Java знает некоторую хорошую оптимизацию, которую gcc не применяет даже с O3. Я сперва подумал, что речь идет о векторизации цикла и добавил в j-й итерации ссылку на arr[j-1], но резульат оказался примерно тем же.

Вот если бы мне это объяснили, я был бы очень признателен.

А насчет выделения памяти — я абсолютно уверен в том, что сборщик мусора в реальной математической программе создает замедление — я с этим сталкивался непосредственно. И то, что я не смог это проиллюстрировать наглядно, не отменяет этого факта. Я действительно не силен в искусственных бенчмарках.
А насчет выделения памяти — я абсолютно уверен в том, что сборщик мусора в реальной математической программе создает замедление — я с этим сталкивался непосредственно. И то, что я не смог это проиллюстрировать наглядно, не отменяет этого факта. Я действительно не силен в искусственных бенчмарках.

Обычно стараются переиспользовать тяжелые объекты (равно как и при использовании того же blas/lapack/arpack в коде на Си или numpy/scipy в случае пайтона). А выделение небольших короткоживущих объектов в рамках TLAB — операция дешевая (выделение реализуется увеличением значения указателя на размер выделяемого объекта + проверка на выход за границу имеющегося блока).


Т. е. если писать на яве "на фортране" и с предвыделенными массивами, то всё очень неплохо. Правда, такой код не сильно отличается от аналогичного на плюсах и обычно пишется на яве ради удобства интеграции с другой частью ява-кода. Другое дело, что ручное использование интринзиков или код, скомпилированный icc с -xHost и т. п., могут дать ещё дополнительный буст по производительности.

Правда, такой код не сильно отличается от аналогичного на плюсах и обычно пишется на яве ради удобства интеграции с другой частью ява-кода.

Вот так обычно и писалось. Но главное преимущество для меня — удобство отладки кода. Отладчик Java непревзойден.

Важный момент: Java обязательно обнуляет всю выделенную память. Этого требует модель безопасности. Соответственно она реально пишет в те страницы, которые возвращаются при выделении. Для зануления в C++ используйте new int[1024 * 1024](). Так будет честнее.

Да, спасибо! Разумеется, я про это знал, но не подумал. В любом случае, меня удивляет, что Java быстрее, а не то, что она медленнее.
Нет, дело не в оптимизациях. Выделение памяти работает по-другому.

Если попросить достаточно большой кусок памяти (согласно документации, более 128 KB), то new в C++ сводится к системному вызову mmap, который не выделяет память, а лишь резервирует виртуальное адресное пространство. Вся магия происходит при первом обращении к зарезервированной странице: случается page fault, в ответ на который ядро уже коммитит данную страницу, то есть, размещает её в физической памяти.

Стало быть, в тесте на C++ реальное выделение памяти неявно скрыто в обращении к массиву. А если «трогать» лишь первые 100 KB массива, как происходит во втором тесте, то и физически размещено в RAM будет лишь 100 KB. В Java же выделение «честное»: аллоцировали 1 MB — столько же сразу и будет закоммичено. При этом вся область будет ещё обязательно обнулена.

Сделай отдельный доклад про кривые бенчмарки, у тебя материала уже много :-)

Интересное кино. Какими кусками выделяется память при обращении к элементам массива? Содается впечатление, что кусочки очень маленькие, потому что выглядит так, как будто при проходе по массиву C++ чуть ли не равномерно идет, со скоростью ниже, чем Java.

Надо бы померить точно для произвольной длины…
Страницами, то есть, по 4 КБ. Однако в случае, когда идёт последовательное обращение к виртуальной памяти, ОС может использовать упреждающее чтение, то есть, коммитить последующие страницы заранее.
Эх, вот смотрю на эти вкусности JITа и слюни пускаю, что шарповый умеет разве что вместо интерфейсного метода вызывать его на инстансе, избегая упаковки в случае вызова на структуре…
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Изменить настройки темы

Истории