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

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

Неужели есть в мире приложения в которых разница в скорости между for и foreach будет иметь хоть сколь-нибудь важное значение?
А уж сравнивать задержка на 1, 10 или 100 проходах вообще странно. Естественно время, затраченное на инициализацию будет значительно влиять.
Ну и конечно habrahabr.ru/post/192130/
Интересная статья, жаль не попалась мне раньше. Что впрочем не отменяет ценность этой статьи. В вашей ссылке написано, что List.ForEach показал худший результат. Здесь же видно, что для длинных списков его производительность такая же как у for-loop. Плюс её можно записывать более элегантно и использовать делегаты.
У foreach есть побочный эффект в виде создания объекта итератора, что на мобильных платформах может аукнуться при интенсивном использовании циклов.
Для List и прочих встроенных типов коллекций, энумераторы обхявлены как value types, поэтому их создание не несет существенных накладных расходов. Плюс, они создаются по штуке на цикл (а не итерацию).
Про value types не знал, если это так и итераторы создаются на стеке, тогда да проблем быть не должно.
Вы бы сравнили с foreach С# 5 т.к. он разворачивается в чуть более ресурсоемкую конструкцию:
{
    E e = ((C)(x)).GetEnumerator();
    try {
        while (e.MoveNext()) {
            V v = (V)(T)e.Current;
            embedded-statement
        }
    }
    finally {
        … // Dispose e
    }
}

вместо того как было:

{
    E e = ((C)(x)).GetEnumerator();
    try {
        V v;
        while (e.MoveNext()) {
            v = (V)(T)e.Current;
            embedded-statement
        }
    }
    finally {
        … // Dispose e
    }
}

А всё из-за обезьян, которые не смогли осилить замыкание.
Ну это всё-таки перевод, а Я работаю на C# 3.2.4.
Почитал про foreach C# 5, и что-то мне кажется, что это фича, а не баг.
Поэтому я ой не торопился бы про обезьян.
Ткните пальцем туда, где я написал что это баг. Эрик Липперт в своём блоге как раз и писал, что они это сделали из-за «баг репортов» тех самых обезьян, которые не понимают что такое замыкание.
Ясно. Лично я подумал, что про обезьян — это вы о разработчиках компилятора :)
Все равно ваша логика непонятна. То, как сделано в C# 5 — это как раз так, как оно должно было быть с самого начала, при наличии замыканий. Но замыканий в С# 1.0 не было, поэтому сделали как проще — а когда их добавили в 2.0, то это решение пересмотрено не было.

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

К производительности это все не имеет никакого отношения, т.к. переменная не «выделяется» на каждой итерации цикла.
Было: поведение контринтуитивное и совершенно бесполезное для программистов. Хак компилятора наружу, протекшая абстракция.
Стало: поведение предполагаемое любым разумным человеком не читавшим описание foreach и удобное для программистов.

А вот почему вдруг новое поведение более ресурсоемкое… тут знаете надо оптимизатор точить, а не программистов обезьянами называть.
Во-первых, это breaking changes, во вторых — раньше оно работало так же как и for, в котором замыкание всё ещё есть.
Во-первых, это breaking changes

xkcd.com/1172

раньше оно работало так же как и for

А сейчас оно работает так же, как ForEach, что гораздо логичнее.
Может кто-то и использует регулярно for в своей работе, но я, к счастью, с такой необходимостью давно не встречался.
>>А сейчас оно работает так же, как ForEach, что гораздо логичнее.
Чтоб вы знали, List::ForEach сами MS считают злом (и намеренно не делали LINQ расширение) и успешно выпилили из Windows 8 этот метод из листа. Если хотите, могу скинуть сцылку на соответсвующую блогозапись Эрика Липперта.
Хорошо, вы меня вынудили таки почитать его статьи по этому поводу.

Притензии к ForEach у него исключительно семантические: LINQ не для сайдэффектов. Не могу с ним не согласиться, особенно в синтаксисе C#.

Про foreach же он высказывается весьма однозначно:
Also, it seems reasonable that the user of the foreach loop might think of there being a «fresh» loop variable every time, not just a fresh value in the same old variable. Since the foreach loop variable is not mutable by user code, this reinforces the idea that it is a succession of values, one per loop iteration, and not «really» the same variable over and over again.

Тут же указано (жирный шрифт) существенное отличие foreach от for.

Да, он приводит несколько причин для существования текущего поведения, но я готов с ними поспорить. Моим тезисом было бы ассоциированные foreach с ForEach, в не с for. Так, сделано, например, в scala.

Ну и да, immutable переменная при замыкании должна иметь поведение копирования значения.
Нет, не работало.
> ForEach-петлям
> петлям
Циклы же…
тесты TestListFor и TestArrayFor считают сумму индексов. Остальные тесты — считают сумму элементов списка/массива.
static void TestArrayFor()
{
    long totalValue = 0;
    for (int i = 0; i < g_IntArray.Length; i++)
        totalValue += i;
}

static void TestListFor()
{
    long totalValue = 0;
    for (int i = 0; i < g_IntList.Count; i++)
        totalValue += i;
}


Поэтому основной вывод статьи
Как видно, для небольших списков лучше всего использовать стандартный цикл с внешним счётчиком. По производительности ForEach проигрывает ему в разы.

мягко выражаясь, некорректен.

List.ForEach на моей машине (с) работает медленее, чем foreach, если totalValue используется после цикла (даже в виде return totalValue)

Микрооптимизация — [почти всегда] зло.
Поэтому основной вывод статьи мягко выражаясь, некорректен. Не в разы.


Этот вывод я делал не на основании TestListFor и TestArrayFor, а на основании написанного мной скрипта.
Ок. тогда статья некорректна только до примечаний переводчика.
Цифры в оригинальной статье сходятся с данными полученными мной с помощью собственного скрипта, в котором считаются не индексы, а элементы. Так что не знаю, в чём у вас дело.
У меня как раз все нормально. на 100КК элементах, 20 итерациях, релиз, x64 с оптимизациями.
for — 220ms
List.ForEach — 381ms (который на самом деле for-no-count + вызов метода)
foreach — 401ms

Вот только на другой машине результаты будут совсем другими (скорее всего с перевесом в пользу стандартного for). То, что у вас цифры сошлись с результатами некоректного теста, запущенного кем-то 8 лет и 2.5 версий фреймворка назад, отлично это подтверждает. У меня, например, не сошлись. Но я не делаю их этого глобальных выводов.
С чем вы собственно спорите?

for работает быстрее List.ForEach, который в свою очередь работает быстрее foreach, что показывают и ваши данные.

И вообще это данные для корректного или некорректного теста?
Это данные для корректного теста.
for естественно будет работать быстрее, чем List.ForEach — но для того, чтобы это доказать, не нужно ничего тестировать и рисовать графики. Достаточно просто заглянуть в исходники List.ForEach.

На вашем скрипте ситуация совсем не та, которую тестирует автор оригинальной статьи — вы тестируете расходы на сам факт запуска цикла. Автор оригинала тестировал скорость перебора элементов. Это две совсем разные метрики (и сняты они на разных платформах). А вы пытаетесь использовать свои результаты как подтверждение оригинальных замеров.

List.ForEach быстрее, чем foreach — очень спорное утверждение. Разница между 381ms и 401ms на 100КК итераций, на практически пустом цикле, настолько незначительна, что при разработке можно спокойно выбирать то, что подходит по семантике — на производительность это не повлияет вообще никак. Практически, разницы нет. А если и есть — то она может качнуться в другую сторону на те же 20ms в зависимости от фазы луны.
for естественно будет работать быстрее, чем List.ForEach — но для того, чтобы это доказать, не нужно ничего тестировать и рисовать графики. Достаточно просто заглянуть в исходники List.ForEach.

Практика подтверждает теорию, а не наоборот. На практике, же согласно оригинальной статье, for без оптимизации компилятора работает медленнее List.ForEach.

На вашем скрипте ситуация совсем не та, которую тестирует автор оригинальной статьи — вы тестируете расходы на сам факт запуска цикла. Автор оригинала тестировал скорость перебора элементов. Это две совсем разные метрики (и сняты они на разных платформах).

Честно неуловил, в чём вы нашли отличие цикла от перебора?

List.ForEach быстрее, чем foreach — очень спорное утверждение. Разница между 381ms и 401ms на 100КК итераций, на практически пустом цикле, настолько незначительна, что при разработке можно спокойно выбирать то, что подходит по семантике — на производительность это не повлияет вообще никак. Практически, разницы нет. А если и есть — то она может качнуться в другую сторону на те же 20ms в зависимости от фазы луны.

То вы говорите, что нужно смотреть в исходники, то на фазу луны. Автор сделал предположение:
Он будет вызывать callvirt IL инструкцию, но два вызова медленнее, чем один callvirt. Итак, я ожидаю, что List<T>.ForEach на самом деле будет быстрее.

После чего провёл вычислительный эксперимент, при разных условиях, подтверждающий его теорию. Вы тоже провели эксперимент показывающий, хоть и не такое большое, но превосходство ForEach, но при этом утверждаете, что List.ForEach быстрее, чем foreach — очень спорное утверждение. В чём же его спорность?
Практика подтверждает теорию, а не наоборот. На практике, же согласно оригинальной статье, for без оптимизации компилятора работает медленнее List.ForEach.

Цикл for (который написан внутри List.ForEach) находится в mscorlib.dll. Которая, естественно, скопилирована с оптимизацией, и для которой заранее сгерерирован не-отладочный native image. Т.е. тест «без оптимизации» на самом деле сравнивает «for без оптимизации и без bounds check elimination» и «for c вызовом action c оптимизацией». Последний, естественно, оказывается быстрее. Так что практика вполне подтверждает теорию.
Честно неуловил, в чём вы нашли отличие цикла от перебора?

Автор вызывает цикл один раз (и один раз тратит время на создание энумератора, поэтому цена создания энумератора вообще никак не влияет на его результаты). Вы вызываете цикл 1000 раз (или больше), 1000 раз тратите время на создание энумератора, да еще и измеряете затраты на внешний цикл и switch (насколько я понял). Поэтому у вас в левой стороне графика такие сильные расхождения. Ну и опять же — JIT на вашей платформе ведет себя совсем не так, как JIT на платформе автора. Вы сравниваете разные вещи, просто графики получились похожие.
В чём же его спорность

Спорность в том, что сравнивать только средние значения некорректно. Нужно учитывать разброс результатов.
Вот результаты с моей машины (с) на 20 попытках:
ForEach: среднее 367.9, отклонение (оценочное) 13.7. 95% попыток будут в диапазоне 354-381.
foreach: среднее 373.8, отклонение (оценочное) 16.8. 95% попыток будут в диапазоне 356-390.
Средние рядом, разброс значений гораздо больше чем разница средних.
С достаточно большой вероятностью конкретный запуск foreach где-то в приложении будет быстрее (не намного, но быстрее) конкретного запуска ForEach.
В моей выборке есть пара значений с разницей в 39ms (10%!) в пользу foreach. Можно это засчитать как аргумент в пользу спорности?
Цикл for (который написан внутри List.ForEach) находится в mscorlib.dll. Которая, естественно, скопилирована с оптимизацией, и для которой заранее сгерерирован не-отладочный native image. Т.е. тест «без оптимизации» на самом деле сравнивает «for без оптимизации и без bounds check elimination» и «for c вызовом action c оптимизацией». Последний, естественно, оказывается быстрее. Так что практика вполне подтверждает теорию.

Очень интересно, не знал.

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

Ну да, я вызываю энумератор около миллиона раз, чтобы получить усреднённое значение. В таком случае, затратами на внешний цикл и switch можно пренебречь.

Спорность в том, что сравнивать только средние значения некорректно. Нужно учитывать разброс результатов.
Вот результаты с моей машины (с) на 20 попытках:
ForEach: среднее 367.9, отклонение (оценочное) 13.7. 95% попыток будут в диапазоне 354-381.
foreach: среднее 373.8, отклонение (оценочное) 16.8. 95% попыток будут в диапазоне 356-390.
Средние рядом, разброс значений гораздо больше чем разница средних.
С достаточно большой вероятностью конкретный запуск foreach где-то в приложении будет быстрее (не намного, но быстрее) конкретного запуска ForEach.
В моей выборке есть пара значений с разницей в 39ms (10%!) в пользу foreach. Можно это засчитать как аргумент в пользу спорности?

Хорошие данные, которые опять же показывают, что ForEach немного, но быстрее foreach.
>Цикл for (который написан внутри List.ForEach) находится в mscorlib.dll. Которая, естественно, скопилирована с оптимизацией, и для которой заранее сгерерирован не-отладочный native image. Т.е. тест «без оптимизации» на самом деле сравнивает «for без оптимизации и без bounds check elimination» и «for c вызовом action c оптимизацией». Последний, естественно, оказывается быстрее. Так что практика вполне подтверждает теорию.

Не по-этому.

List внутри использует массив T[]. Для массивов в CLI есть специальные опкоды (получение длинны, получение элемента и установка элемента). Если вы делаете for по List, то элементы будут получаться через индексатор, а длина через свойство, причем через виртуальный вызов.

Для массива:

IL_0000: nop
IL_0001: ldc.i4.0
IL_0002: stloc.0
IL_0003: br.s IL_0017
// loop start (head: IL_0017)
IL_0005: nop
IL_0006: ldarg.1
IL_0007: ldloc.0
IL_0008: ldelem.i4
IL_0009: stloc.1
IL_000a: ldarg.0
IL_000b: ldloc.1
IL_000c: call instance void ClassLibrary2.Class1::Work(int32)
IL_0011: nop
IL_0012: nop
IL_0013: ldloc.0
IL_0014: ldc.i4.1
IL_0015: add
IL_0016: stloc.0

IL_0017: ldloc.0
IL_0018: ldarg.1
IL_0019: ldlen
IL_001a: conv.i4
IL_001b: clt
IL_001d: stloc.2
IL_001e: ldloc.2
IL_001f: brtrue.s IL_0005
// end loop

IL_0021: ret

для списка:

IL_0000: nop
IL_0001: ldc.i4.0
IL_0002: stloc.0
IL_0003: br.s IL_001b
// loop start (head: IL_001b)
IL_0005: nop
IL_0006: ldarg.1
IL_0007: ldloc.0
IL_0008: callvirt instance !0 class [mscorlib]System.Collections.Generic.List`1::get_Item(int32)
IL_000d: stloc.1
IL_000e: ldarg.0
IL_000f: ldloc.1
IL_0010: call instance void ClassLibrary2.Class1::Work(int32)
IL_0015: nop
IL_0016: nop
IL_0017: ldloc.0
IL_0018: ldc.i4.1
IL_0019: add
IL_001a: stloc.0

IL_001b: ldloc.0
IL_001c: ldarg.1
IL_001d: callvirt instance int32 class [mscorlib]System.Collections.Generic.List`1::get_Count()
IL_0022: clt
IL_0024: stloc.2
IL_0025: ldloc.2
IL_0026: brtrue.s IL_0005
// end loop

IL_0028: ret
IL код не показывает полной картины. Смотрите disassembly:
код на C#:
static long TestListFor()
{
    Debugger.Launch();
    long totalValue = 0;
    for (int i = 0; i < g_IntList.Count; i++)
    {
        totalValue += g_IntList[i];
    }
    return totalValue;
}

IL:
.method private hidebysig static int64  TestListFor() cil managed
{
  // Code size       47 (0x2f)
  .maxstack  3
  .locals init ([0] int64 totalValue,
           [1] int32 i)
  IL_0000:  call       bool [mscorlib]System.Diagnostics.Debugger::Launch()
  IL_0005:  pop
  IL_0006:  ldc.i4.0
  IL_0007:  conv.i8
  IL_0008:  stloc.0
  IL_0009:  ldc.i4.0
  IL_000a:  stloc.1
  IL_000b:  br.s       IL_0020
  IL_000d:  ldloc.0
  IL_000e:  ldsfld     class [mscorlib]System.Collections.Generic.List`1<int32> ForEachTest.Program::g_IntList
  IL_0013:  ldloc.1
  IL_0014:  callvirt   instance !0 class [mscorlib]System.Collections.Generic.List`1<int32>::get_Item(int32)
  IL_0019:  conv.i8
  IL_001a:  add
  IL_001b:  stloc.0
  IL_001c:  ldloc.1
  IL_001d:  ldc.i4.1
  IL_001e:  add
  IL_001f:  stloc.1
  IL_0020:  ldloc.1
  IL_0021:  ldsfld     class [mscorlib]System.Collections.Generic.List`1<int32> ForEachTest.Program::g_IntList
  IL_0026:  callvirt   instance int32 class [mscorlib]System.Collections.Generic.List`1<int32>::get_Count()
  IL_002b:  blt.s      IL_000d
  IL_002d:  ldloc.0
  IL_002e:  ret
} // end of method Program::TestListFor


Реально выполняемый код (кусок от зануления аккумулятора до jump на начало списка. Нет ни одного call, выполняется тривиальный цикл по элементам:
; totalValue = 0
00007FFA38F30441  xor         ecx,ecx  
; for
; проверка i < list.Count и границ внутреннего массива
00007FFA38F30443  mov         r9,0BF57335778h  
00007FFA38F3044D  mov         r9,qword ptr [r9]  
00007FFA38F30450  mov         eax,dword ptr [r9+18h]  
00007FFA38F30454  cmp         edx,eax  
00007FFA38F30456  jl          00007FFA38F30460  
00007FFA38F30458  mov         rax,rcx  
00007FFA38F3045B  jmp         00007FFA38F30486  
00007FFA38F3045D  nop         dword ptr [rax]  
00007FFA38F30460  cmp         edx,eax  
00007FFA38F30462  jae         00007FFA38F30495  
00007FFA38F30464  mov         r9,qword ptr [r9+8]  
00007FFA38F30468  movsxd      r10,r8d  
00007FFA38F3046B  mov         rax,qword ptr [r9+8]  
00007FFA38F3046F  cmp         r10,rax  
00007FFA38F30472  jae         00007FFA38F30490  
; загрузка i-го элемента в eax, в IL выглядело как callvirt instance !0 class [mscorlib]System.Collections.Generic.List`1::get_Item(int32)
00007FFA38F30474  mov         eax,dword ptr [r9+r10*4+10h]  
00007FFA38F30479  movsxd      rax,eax  
; totalValue += rax
00007FFA38F3047C  add         rcx,rax  
00007FFA38F3047F  inc         edx  
00007FFA38F30481  inc         r8d  
; jump на начало for
00007FFA38F30484  jmp         00007FFA38F30443  


JIT знает о стандартных генериках. Т.е. в IL callvirt есть — а при выполнении его уже нет.
Для проверки лучше цеплять дебаггер уже в процессе, после того, как JIT отрабтал. Иначе получите код без JIT оптимизаций (т.е. медленный и с call).
Для сравнения, вот во что JIT превращает тот же код, если отладчик прицеплен с самого начала:
00007FFA38F54014  mov         qword ptr [rsp+20h],0  
00007FFA38F5401D  mov         dword ptr [rsp+28h],0  
00007FFA38F54025  jmp         00007FFA38F54079  
00007FFA38F54027  mov         rcx,0B42D95E728h  
00007FFA38F54031  mov         rcx,qword ptr [rcx]  
00007FFA38F54034  mov         rax,qword ptr [rsp+20h]  
00007FFA38F54039  mov         qword ptr [rsp+30h],rax  
00007FFA38F5403E  mov         qword ptr [rsp+38h],rcx  
00007FFA38F54043  mov         rax,qword ptr [rsp+38h]  
00007FFA38F54048  cmp         byte ptr [rax],0  
00007FFA38F5404B  mov         rcx,qword ptr [rsp+38h]  
00007FFA38F54050  mov         edx,dword ptr [rsp+28h]  
; вызов get_Item(int32)
00007FFA38F54054  call        00007FFA9743F430  
00007FFA38F54059  mov         dword ptr [rsp+40h],eax  
00007FFA38F5405D  movsxd      rcx,dword ptr [rsp+40h]  
00007FFA38F54062  mov         rax,qword ptr [rsp+30h]  
; totalValue += element
00007FFA38F54067  add         rax,rcx  
00007FFA38F5406A  mov         qword ptr [rsp+20h],rax  
00007FFA38F5406F  mov         eax,dword ptr [rsp+28h]  
00007FFA38F54073  inc         eax  
00007FFA38F54075  mov         dword ptr [rsp+28h],eax  
00007FFA38F54079  mov         rcx,0B42D95E728h  
00007FFA38F54083  mov         rcx,qword ptr [rcx]  
00007FFA38F54086  mov         eax,dword ptr [rsp+28h]  
00007FFA38F5408A  mov         dword ptr [rsp+44h],eax  
00007FFA38F5408E  mov         qword ptr [rsp+48h],rcx  
00007FFA38F54093  mov         rax,qword ptr [rsp+48h]  
00007FFA38F54098  cmp         byte ptr [rax],0  
00007FFA38F5409B  mov         rcx,qword ptr [rsp+48h]  
; вызов Count и проверка i < Count
00007FFA38F540A0  call        00007FFA9743F470  
00007FFA38F540A5  mov         dword ptr [rsp+50h],eax  
00007FFA38F540A9  mov         eax,dword ptr [rsp+50h]  
00007FFA38F540AD  cmp         dword ptr [rsp+44h],eax  
00007FFA38F540B1  jl          00007FFA38F54027  

Для небольших списков нужно использовать foreach, потому что он куда как понятней, и отлавливает ошибки вроде изменения коллекции, а разница в производительности здесь мизерная.

Для больших списков лучше, опять же, использовать foreach, если нет каких-либо веских доводов делать что-то другое (т.е. — конкретная проблема с производительностью в данном месте, решаемая подобными микрооптимизациями).
Результат выполнения программы автора у меня (release-mode):
Initializing 100,000,000 items…

Testing for on int[]…
Executed in 0.275374 seconds.

Testing foreach on int[]…
Executed in 0.485148 seconds.

Testing Array.ForEach…
Executed in 0.528751 seconds.

Testing for on List…
Executed in 0.569238 seconds.

Testing foreach on List…
Executed in 0.548733 seconds.

Testing List.ForEach…
Executed in 0.534959 seconds.
Ну как уже было замечено в комментах TestListFor и TestArrayFor считают сумму индексов, видимо автор выложил не ту версию исходников.
Надо подредактировать эти функции
static void TestArrayFor()
{
    long totalValue = 0;
    for (int i = 0; i < g_IntArray.Length; i++)
        totalValue += g_IntArray[i];
}

static void TestListFor()
{
    long totalValue = 0;
    for (int i = 0; i < g_IntList.Count; i++)
        totalValue += g_IntArray[i];
}
В общем, примерно как я и ожидал, ну не может быть List(T).ForEach быстрее
image

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

В общем, ИМХО на 4.0 и выше — неактуально. Цифры выше приведены для 4.5.1 release.

Кстати, вопрос знатокам: а почему по списку настолько дольше обход? Я думал, он на этапе обхода ведет себя как обычный массив — ведь внутри это и есть просто массив. Такой большой оверхед из-за чего? Лишь из-за проверок границ массива?
> Причем только по 1 циклу на каждый метод
> нету GC.Collect

Я правильно понял, что это как раз ваши циферки в консоли?
GC.Collect я вставил, но он не учитывается во времени. Вот так:
        private static void RunTest(string header, TestMethod testMethod)
        {
            HighResolutionTimer timer = new HighResolutionTimer();
            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();

            Console.WriteLine(header);
            timer.Start();
            testMethod();
            timer.Stop();
            Console.WriteLine("Executed in {0}.", timer);
            Console.WriteLine();
        }

ну и исправил сумму не индексов, а элементов по индексам, как подсказали выше.
остальное все так же.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории