- BrainTools - https://www.braintools.ru -

Стал ли код Microsoft хуже с приходом ИИ: замеры .NET-библиотек от 6.0 до 11 RC

Линии 46 операций от .NET 7.0 до 11 RC: синие уходят вниз, красные вверх, IOptionsSnapshot и биндинг POCO резко взлетают на 11 RC

Линии 46 операций от .NET 7.0 до 11 RC: синие уходят вниз, красные вверх, IOptionsSnapshot и биндинг POCO резко взлетают на 11 RC

В ноябре 2022 года с разницей в три недели вышли .NET 7 и ChatGPT. Получилась удобная точка отсчёта: .NET 7 стал последним релизом, написанным до ChatGPT. А в мае 2025 года в dotnet/runtime пришёл Copilot coding agent и к концу октября 2026-го открыл там 1734 pull request’а, 1207 из них смержены.

dПортит ли ИИ код, спорить можно долго, а производительность можно просто измерить. Я замерил DI, конфигурацию, логирование, кэш и EF Core: 46 операций, шесть мажорных версий, 30 раундов. Заодно по истории Git проверил, кто трогал горячие методы, а две просадки в 11 RC проследил до конкретных коммитов. Самую крупную внёс человек одной строкой. Вторая, поменьше, пришла с PR от Copilot coding agent, который чинил дедлок.

Как устроены замеры

Шкала времени: 6.0 и 7.0 до ChatGPT, 8.0 и 9.0 в эпоху ассистентов, 10.0 и 11 RC3 после первого PR агента

Шкала времени: 6.0 и 7.0 до ChatGPT, 8.0 и 9.0 в эпоху ассистентов, 10.0 и 11 RC3 после первого PR агента

Новый .NET ускоряется ещё и за счёт ASP.NET [1] Core), так не подменить: старый пакет проигрывает встроенной сборке. Их, как и EF Core 11, я сравниваю отдельно, net10.0 против net11.0, и там разделить библиотеку и рантайм уже нельзя.

Вот один из замеров, IOptionsSnapshot в новом scope:

recorder.Measure(OptionsScopes.Snapshot, 1000, () =>
{
    for (var i = 0; i < 1000; i++)
    {
        using var scope = provider.CreateScope();
        Sink.Keep(scope.ServiceProvider.GetRequiredService<IOptionsSnapshot<Poco50>>().Value);
    }
});

Время пишет EmberTrace [2], а память [3] считает GC.GetAllocatedBytesForCurrentThread. На процесс приходится 20 прогревочных и 200 измеряемых итераций, каждый сценарий запускается в своём процессе и привязан к логическим ядрам 2–5. Всего 30 раундов и 111 процессов на каждую пару версии и сценария, порядок версий в каждом раунде перемешан.

Единица наблюдения — процесс, все раунды весят одинаково. Изменение считается внутри раунда относительно 7.0, доверительный интервал даёт иерархический бутстрэп, значимость — U-критерий Манна — Уитни с поправкой Бенджамини — Хохберга. «Быстрее» или «медленнее» ставится, только если весь 95-процентный интервал лежит за порогом в 5%, ни один раунд не показывает обратного и скорректированное p меньше 0,05. Если весь интервал лежит внутри ±5%, это шум, а всё остальное остаётся без вердикта.

Раунды шли в двух состояниях одного ноутбука (Core i5-12450H, Ubuntu 22.04). «Стенд» — это governor performance и выключенный турбобуст, 19 раундов. «Заводские настройки» — powersave с турбобустом, 11 раундов. Почему это важно, разберём ниже. Если не сказано иное, цифры дальше относятся к стенду.

Прогретый код ускорился, первый вызов замедлился

Для версии 10.0 против 7.0 итог по 46 операциям такой: 9 быстрее, 13 медленнее, 10 в пределах шума, по 14 вердикта нет. На графике показательные примеры с обеих сторон: повторяющиеся операции к 10.0 ускорились на 14–35%, а первые вызовы и создание объектов замедлились на 39–64%.

Восемь линий из одной точки 7.0: четыре прогретые операции к версии 10.0 уходят вниз на 14–35 процентов, четыре операции первого использования растут на 39–64 процента

Восемь линий из одной точки 7.0: четыре прогретые операции к версии 10.0 уходят вниз на 14–35 процентов, четыре операции первого использования растут на 39–64 процента

В единицах времени это выглядит спокойнее. Значения сняты на ноутбуке с частотой 2 ГГц, так что смотреть стоит на порядок величин:

Операция

7.0

10.0

LoggerMessage.Define, один вызов

377 нс

244 нс

MemoryCache.TryGetValue, попадание

111 нс

96 нс

EF DetectChanges, 10 тыс. сущностей

6,1 мс

4,3 мс

DI, первый resolve графа глубиной 5

3,3 мкс

5,2 мкс

Сборка провайдера с ValidateOnBuild, 100 регистраций

40 мкс

66 мкс

Новый LoggerFactory

23 мкс

34 мкс

Новый DbContext

37 мкс

51 мкс

EF, самый первый запрос в процессе

996 мс

1162 мс

Для долгоживущего сервиса такой размен выгоден: логирование выполняется миллионы раз, а провайдер собирается один раз. Короткоживущим процессам повезло меньше, особенно с EF: первый запрос подорожал на 166 мс, и это уже заметная прибавка к холодному старту. Правда, холодные замеры включают JIT, а старые пакеты работают на новом рантайме, так что у этой строки погрешность выше остальных.

Обе тенденции видны и на заводских настройках, но там раундов меньше, и большинство замедлений первого вызова формально остаётся без вердикта. Уверенно подтверждается только LoggerFactory: +49%.

Больше всего изменилось в 8.0

В споре обычно звучит «до нейросетей было лучше», поэтому посмотрим на шаги между соседними релизами.

Четыре ряда точек по шагам между релизами: подтверждено 19 сдвигов из 46 на 7.0→8.0, 5 на 8.0→9.0, 5 на 9.0→10.0 и 6 из 38 на 10.0→11 RC

Четыре ряда точек по шагам между релизами: подтверждено 19 сдвигов из 46 на 7.0→8.0, 5 на 8.0→9.0, 5 на 9.0→10.0 и 6 из 38 на 10.0→11 RC

Правила вердикта здесь те же, только базой служит предыдущий релиз: подтверждается 19 сдвигов из 46 на шаге к 8.0, по 5 на шагах к 9.0 и 10.0 и 6 из 38 на шаге к 11 RC. Шаг к 10.0 на фоне предыдущего не выделяется, правда, агент застал только последние полгода его разработки.

На заводских настройках 8.0 так не выделяется (8, 7, 5 и 6): там всего 11 раундов, и интервалы в разы шире. Зато шаг к 11 RC совпадает. В обоих режимах подтверждено по 6 сдвигов, и 5 операций из 6 общие.

К самому переходу на 8.0 есть возражение. Каждая мажорная версия пакета приносит свою сборку: lib/net7.0 у 7.x, lib/net8.0 у 8.x, хотя рантайм везде .NET 10. Значит, в шаг входят и ветки условной компиляции, которых старая сборка не видит даже на новом рантайме. Это тоже код библиотеки, но разделить правки и смену целевого фреймворка такой дизайн не позволяет.

На шаге к 10.0 выделяются три операции. new DbContext подорожал на 13%, Compact в кэше подешевел на 13%, а DetectChanges стал на 19% медленнее, чем в 9.0, и отдал часть выигрыша: относительно 7.0 было −41%, стало −30%.

То, что самый крупный сдвиг случился в первом релизе после ChatGPT, ничего не говорит об участии ИИ: замеры показывают, что и когда поменялось, но не почему.

Агент: до 10.0 в горячих методах DI и кэша его нет, в 11.0 он там появился

Каждый коммит DI и Caching.Memory между тегами v7.0.20 и v10.0.14 сопоставлен с его pull request’ом. Агента легко опознать: PR открывает GitHub-приложение copilot-swe-agent с id 198982749, а в squash-коммитах остаётся трейлер Co-authored-by. Нашлось вот что:

  • 113 коммитов: 57 от участников организации, условно Microsoft, 56 от сообщества, от агента ни одного, соавторских с агентом тоже ни одного;

  • 24 горячих метода, прослеженных через git log -L: 39 правок, агентских среди них нет, а 8 методов между этими тегами не менялись вообще;

  • 6 PR с меткой tenet-performance, все написаны людьми.

Запланированное сравнение «методы, которые трогал агент, против методов, которые трогали только люди» на этом окне провести не получилось: первая группа пуста. Но окно заканчивалось на 10.0, а полный цикл разработки у агента был только в 11.0. Поэтому я дотянул разметку до RC3 и добавил остальные библиотеки. Здесь считается только автор коммита в Git, без разбивки на Microsoft и сообщество:

Библиотека

7.0 → 10.0, коммитов

из них агент

10.0 → 11 RC, коммитов

из них агент

DI

80

0

21

11

Кэш

33

0

14

1

Конфигурация

67

0

15

1

Options

37

0

7

0

Логирование

52

1

8

3

EF Core

867

29

256

73

Часть агентских коммитов в DI — документация и аннотации, но не все: из 11 правок горячих методов DI и кэша в новом окне 3 принадлежат агенту. Ещё в 5 коммитах конфигурации и Options есть соавтор Copilot CLI.

Оговорки к таблице:

  • На реальную причастность я проверил только биндер и DI, про них ниже. EF Core только посчитан, хотя правок агента там больше всего, а на шаге к 10.0 заметно сдвинулись его DbContext и DetectChanges.

  • Коммит агента значит «PR открыт агентом», а не «написано без человека»: такие PR проходят ревью и вливаются людьми. Ассистент в IDE вообще не оставляет следов в Git, так что об эпохе ассистентов данные ничего сказать не могут.

  • Граница эпох условна. Автодополнение Copilot появилось ещё в 2021 году, так что и 7.0 мог писаться не совсем без ИИ.

  • Деление на Microsoft и сообщество грубое: оно опирается на поле author_association, и сотрудники со скрытым членством в организации попадают в сообщество. В 24 коммитах из 113 это поле расходится с почтой на @microsoft.com [4].

Самая крупная просадка в 11 RC сидит в одной строке

Столбцы для пяти операций: в 10.0 изменения от минус 3 до плюс 13 процентов, в 11 RC от плюс 26 до плюс 162 процентов

Столбцы для пяти операций: в 10.0 изменения от минус 3 до плюс 13 процентов, в 11 RC от плюс 26 до плюс 162 процентов

Относительно 7.0 биндинг POCO на 50 полей подорожал с 61 до 145 мкс, IOptionsSnapshot в новом scope — с 52 до 136 мкс. Аллокации у обеих операций выросли почти одинаково, примерно на 10 КБ на вызов. На заводских настройках картина та же: +134% и +158%.

Прибавка в 10 КБ на 50 свойств подсказывает, что платим за каждое свойство. В ConfigurationBinder.cs между 10.0 и RC3 всего 7 коммитов, и один из них, #126396 [5] с новым атрибутом [ConfigurationIgnore], добавляет как раз такую проверку:

private static bool IsIgnoredProperty(PropertyInfo property) =>
    property.IsDefined(typeof(ConfigurationIgnoreAttribute));

Она выполняется для каждого свойства при каждом биндинге. Чтобы проверить догадку, я собрал биндер из исходников RC3 с заменой этого выражения на false и подменил одну DLL в уже собранном стенде. Те же сценарии, 6 раундов, режим стенда:

Операция

10.0

11 RC

11 RC, строка заменена

Биндинг POCO

61 мкс

148 мкс

60 мкс

IOptionsSnapshot в новом scope

51 мкс

139 мкс

53 мкс

Валидация DataAnnotations

43 мкс

53 мкс

43 мкс

Аллокации биндинга на вызов

18,4 КБ

28,4 КБ

18,4 КБ

Одна строка объясняет сразу три из шести подтверждённых сдвигов этого шага. Холодную сборку конфигурации (+76%) она не лечит, причина там другая и пока не найдена. Отдельно я проверил это в BenchmarkDotNet, без моего трейсера, на минимальном примере:

Столбцы времени Get<span class=

Столбцы времени Get<T>: 10.0 — 29 мкс, 11 RC3 — 120 мкс, RC3 с inherit false и биндер из main — 45 мкс, RC3 с заглушкой — 29 мкс, генератор исходников — 12 мкс

Основную цену даёт перегрузка-расширение: у неё по умолчанию inherit: true. С inherit: false время падает со 120 до 45 мкс, а аллокации возвращаются к уровню 10.0. Именно так написано в main после #135672 [6], но тот PR чинит другую ошибку [7] и в ветку release/11.0 не попал. Я завёл issue #136759 [8] с примером и этими цифрами. Бенчмарки dotnet/performance этого, похоже, не ловят: их тест биндинга [9] использует класс с одним свойством.

Мейнтейнер ответил в тот же день. Регрессию признали (issue получил метку regression-from-last-release), но сказали прямо: производительность рефлексионного биндера не главный приоритет, для горячих путей рекомендуют генератор исходников. Смягчить большую часть регрессии в main обещали и предложить фикс для бэкпорта в 11.0, а IOptionsSnapshot назвали отдельной проблемой, уже обсуждавшейся в #53793 [10]. Позже появился черновой PR #136899 [11]: он смотрит на флаги виртуальности геттера и сеттера и вызывает дешёвый поиск атрибута без наследования, если свойство переопределить нельзя. По цифрам автора, Get<T> на 50 свойств занимает 10,3 мкс против 27,6 мкс в затронутой сборке и 9,4 мкс в биндере 10.0, аллокации возвращаются к 17,5 КБ. Остаётся примерно 10–15% к 10.0, то есть кэширование на тип, которое я предлагал, этот PR не заменяет и не включает. На момент написания PR не влит и без ревью, так что попадёт ли фикс в 11.0, пока неизвестно. Косвенно об этом говорит и milestone: issue привязан к 12.0.0, а не к 11.0, поэтому бэкпорт пока обещан, но не гарантирован.

PR открыт участником организации, фича делалась по одобренному API-запросу, следов Copilot в коммите нет. Это не доказывает, что ИИ не использовался: ассистент в IDE в Git не виден.

Если вы уже на RC, обход есть: генератор исходников (EnableConfigurationBindingGenerator) в том же примере даёт 12 мкс и 5,7 КБ, то есть быстрее и 10.0 (29 мкс), и любого варианта рефлексионного биндера. Именно его мейнтейнеры и советуют для чувствительных к скорости сценариев, так что рассчитывать на полное устранение разницы в рефлексионном пути не стоит. IOptionsSnapshot пересоздаёт опции на каждый scope намеренно, и ещё в 2021 году Дэвид Фаулер назвал его ловушкой производительности [10]. Там, где не нужно обновление конфигурации в рамках запроса, IOptionsMonitor дешевле и без этой регрессии.

Оговорка: это релиз-кандидат, а в основном треке работает сборка net10.0 из исходников 11.0 на рантайме .NET 10, так что на настоящем net11.0 цифры могут отличаться.

Ещё одну просадку внёс агент, и это размен

Первый resolve в DI на шаге к 11 RC вырос ещё на 15%. Здесь агент действительно рядом: в DI у него 11 коммитов из 21, поэтому я собрал DI из исходников по коммитам и прогнал бисект, 8 раундов на вариант. Скачок появляется ровно на одном коммите:

Сборка DI

Первый resolve, 100 провайдеров

10.0, пакет

0,565 мс

Коммит перед #124331

0,554 мс

#124331 [12], PR агента

0,641 мс

11 RC3, пакет

0,653 мс

11 RC3 с откатом только #124331

0,569 мс

Внутри раунда коммит даёт +16,5% (95% интервал 14,3–18,8, все 8 раундов в одну сторону), а откат на RC3 возвращает к уровню 10.0: разница +1,9%, интервал включает ноль. На холодном resolve аллокации растут с 8848 до 9080 байт. Прогретые singleton, scoped и сборку провайдера коммит не задевает.

Этот PR чинит дедлок [13] при циклических зависимостях через фабрики. Теперь VisitRootCache ведёт [ThreadStatic] HashSet тех, кого сейчас резолвит поток. Цена — примерно 0,9 мкс на первый resolve в новом провайдере, и за исправленный дедлок это разумная плата. PR прошёл ревью и влит людьми.

В моём списке из 24 горячих методов VisitRootCache не было, так что разметка по методам этот случай пропустила, и нашёл его только бисект. Остальные агентские правки горячих методов, например переписанный выбор конструктора, на этот сценарий не влияют: там конструктор один. Другие сценарии я так не проверял.

Второй трек: net10.0 против net11.0

Здесь библиотека и рантайм меняются вместе, и новости есть хорошие: Kestrel стартует на 54% быстрее, холодное подключение EF с созданием схемы ускорилось на 15–17%. DetectChanges там, наоборот, медленнее на 11–12%. Время обработки запроса в ASP.NET [1] Core выросло на 81–90%, но этой цифре я пока не верю: распределение многомодальное, встроенный генератор нагрузки работает с фиксированной частотой, а bombardier не запускался.

Чем всё это снято

EmberTrace — мой трейсер для .NET под лицензией MIT, пакет есть на NuGet. Все основные замеры статьи — это один скоп на пачку вызовов, как в примере в начале: процессов тысячи, каждый оставляет файл .ember, а сравнение строится уже по файлам.

Виновника трейсер сам не назвал: сдвиг показали замеры, подозреваемого нашёл git log, а подтвердила подмена DLL. Зато скоп можно поставить прямо внутрь чужого кода и посмотреть, куда уходит время. Я добавил четыре скопа в исходники биндера:

private static bool IsIgnoredProperty(PropertyInfo property)
{
    using var scope = Tracer.Scope(Tracer.Id("ConfigurationBinder.IsIgnoredProperty"));
    return property.IsDefined(typeof(ConfigurationIgnoreAttribute));
}

Вот отчёт TraceText.Write за 20 пачек по 1000 биндингов:

Name                                   Count    Incl ms   Excl ms   Excl%
bind.x1k                               20       2958.218  192.096   6.44
ConfigurationBinder.BindProperties     20000    2766.122  356.338   11.95
ConfigurationBinder.IsIgnoredProperty  1000000  1813.144  1813.144  60.80
ConfigurationBinder.BindProperty       1000000  551.475   551.475   18.49
ConfigurationBinder.GetAllProperties   20000    45.165    45.165    1.51

Миллион вызовов и 61% времени на проверку атрибута, которого в этом классе нет. За 3 секунды записано 4 млн событий, ни одно не потеряно. У этого есть цена: со скопами биндинг занял 148 мкс против 120 мкс в бенчмарке, то есть грубо 0,3 мкс на скоп вместе с хешированием имени в Tracer.Id [14], так что доли здесь приблизительные. Для такой разовой диагностики это приемлемо, а скоп на каждый вызов в продакшене я бы ставить не стал.

Аллокации надёжнее времени

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

Столбчатая диаграмма аллокаций: от минус 42 процентов у DetectChanges до плюс 51 процента у ToQueryString

Столбчатая диаграмма аллокаций: от минус 42 процентов у DetectChanges до плюс 51 процента у ToQueryString

Часто память и время идут вместе: у LoggerMessage и DetectChanges меньше и того и другого, у ValidateOnBuild и DbContext больше. Но бывает и иначе. Where + OrderBy + Take в EF аллоцирует на 22% больше при времени в пределах шума (+1,7%): в приложении это, скорее всего, вылезет позже как лишняя работа GC. А сборка DI-провайдера на 1000 регистраций требует на 17% больше памяти, хотя её среднее время упало на 25%.

Код при этом растёт. Между 7.0 и 11 RC код DI вырос на 25%, Caching.Memory на 24%, DI.Abstractions на 129%, причём у последней основной скачок приходится на 8.0, где появились keyed services. Средняя цикломатическая сложность почти не сдвинулась, она держится в диапазоне 1,3–2,5, так что по средним всё спокойно. Но максимальная сложность метода в DI выросла с 14 до 23, а в DI.Abstractions с 10 до 26.

«Настроенный» стенд оказался почти вдвое медленнее

Рекомендация для бенчмарков стандартная: зафиксируй частоту, выключи турбобуст. На этом процессоре no_turbo ограничивает ядра базовыми 2 ГГц при максимуме 4,4 ГГц. В результате на заводских настройках все 46 операций базовой версии выполняются в 1,6–2,1 раза быстрее, чем на «правильном» стенде.

Ладно бы менялись только абсолютные числа, но меняются и относительные:

Гантельный график: для операций DI и IOptionsMonitor два режима питания расходятся на 9–14 процентных пунктов, для LoggerMessage, LoggerFactory и кэша на 2–4

Гантельный график: для операций DI и IOptionsMonitor два режима питания расходятся на 9–14 процентных пунктов, для LoggerMessage, LoggerFactory и кэша на 2–4

Прогретые операции DI и IOptionsMonitor в 10.0 показывают ускорение на 3–8% на стенде и на 15–20% на заводских настройках. Для singleton, transient и open generic это разница между «вердикта нет» и «уверенно быстрее», хотя версия кода та же самая. Правдоподобное объяснение — другое соотношение частоты ядра и скорости памяти, но оно не проверялось.

Списать всё на дрейф нельзя: раунды стенда идут и до, и после блока с заводскими настройками. Сравнение режимов находит значимую разницу в 29 случаях из 252, а сравнение первых и последних раундов стенда между собой — в одном. Дизайн при этом неидеален: заводские раунды идут одним блоком, процессов в них меньше (33 против 78).

Поэтому ±5%, полученные на одном ноутбуке в одном режиме питания, мало что значат. Если сравниваете версии, указывайте режим, а лучше мерьте в двух.

Известные ограничения

  • Правила вердикта я ужесточил после первых раундов. Исходное правило сохранено в данных, а отклонение описано в методике, но решение всё же принято после просмотра данных.

  • Всё снято на ноутбуке с гибридным процессором, без изоляции ядер и при средней нагрузке в основном 1–2,7. Поэтому опираться стоит на отношения внутри раунда, а не на абсолютные времена.

  • Результат иногда зависит от метрики. У cache.set и get-or-create в 10.0 медиана показывает рост на 22–31%, а среднее — падение на 5–9%. Причину двух мод я не установил, поэтому выводов по этим операциям не делаю.

  • Замеры синтетические и охватывают только API, которые есть во всех версиях, так что самые новые быстрые пути в сравнение не попали. Старые пакеты работают на новом рантайме и могут не использовать его оптимизации.

Что в итоге

Обновляться с 7.0, которая и так вне поддержки, имеет смысл и ради горячих путей: выигрыш там реальный. Если у вас serverless или короткоживущие утилиты, цену холодного старта лучше измерить самим. Перед переходом на 11.0 стоит прогнать свои сценарии с биндингом конфигурации и IOptionsSnapshot, причём на финальной версии, а не на RC, и посмотреть, попал ли в релиз фикс из #136899 [11].

На исходный вопрос ответ теперь такой: в том, что я проверил, нет признаков того, что код стал хуже из-за ИИ, но и доказательства обратного нет. До 10.0 агента в горячих методах DI и кэша просто не было. В 11.0 он там есть, и единственная его правка, которую я поймал на замерах, стоит 0,9 мкс на первый resolve в обмен на исправленный дедлок. Самая крупная регрессия релиза — одна строка в PR человека, ради новой фичи.

Открытыми остаются EF Core, где правок агента больше всего, холодная сборка конфигурации и то, сколько из этого изменится к финальному релизу.

Код стенда, данные и методика: dotnet-ai-era-benchmarks [15].

Автор: anechways

Источник [16]


Сайт-источник BrainTools: https://www.braintools.ru

Путь до страницы источника: https://www.braintools.ru/article/38104

URLs in this post:

[1] ASP.NET: http://ASP.NET

[2] EmberTrace: https://github.com/Echways/EmberTrace

[3] память: http://www.braintools.ru/article/4140

[4] microsoft.com: http://microsoft.com

[5] #126396: https://github.com/dotnet/runtime/pull/126396

[6] #135672: https://github.com/dotnet/runtime/pull/135672

[7] ошибку: http://www.braintools.ru/article/4192

[8] issue #136759: https://github.com/dotnet/runtime/issues/136759

[9] их тест биндинга: https://github.com/dotnet/performance/blob/main/src/benchmarks/micro/libraries/Microsoft.Extensions.Configuration/ConfigurationBinderBenchmarks.cs

[10] #53793: https://github.com/dotnet/runtime/issues/53793

[11] черновой PR #136899: https://github.com/dotnet/runtime/pull/136899

[12] #124331: https://github.com/dotnet/runtime/pull/124331

[13] дедлок: https://github.com/dotnet/runtime/issues/88390

[14] Tracer.Id: http://Tracer.Id

[15] dotnet-ai-era-benchmarks: https://github.com/Echways/dotnet-ai-era-benchmarks

[16] Источник: https://habr.com/ru/articles/1104356/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1104356

www.BrainTools.ru

Rambler's Top100