Мучения сборщика мусора. Часть II
От: Сергей Губанов Россия http://sergey-gubanov.livejournal.com/
Дата: 15.11.04 15:17
Оценка:
Недавно я задался вопросом как наибыстрейшим способом "прикончить сборщик мусора". Стал придумывать примеры как бы сборщик мусора так нагрузить, чтобы он "захлебнулся мусором". По возможности, исходный код таких примеров должен быть как можно компактнее, чтобы, как говорится, все было "дешево и сердито".

1) Первое что пришло на ум, это создать N объектов в динамической памяти, таких чтобы они все ссылались друг на друга (включая самих себя), так что количество взаимных ссылок между ними было бы в точности равно N*N — максимально возможное количество (разных) ссылок между N объектами. После создания этого конгломерата объектов и ссылок между ними надо натравить на эту динамическую структуру сборщик мусора и посмотреть что получиться. Сборщик мусора, по идее, должен был бы "подавиться" распутывая эти N^2 взаимоссылок.

2) Второе что пришло на ум была совсем тривиальная вещь: создать N объектов в динамической памяти, ссылки на них записать в массив, а потом в цикле много раз создавать N+1'вый объект и записывать ссылку на него в этот самый массив вместо какой-то другой ссылки, старая ссылка, стало быть теряется и сборщик мусора должен тот старый объект уничтожить, таким образом объектов опять должно остаться ровно N штук.

Эксперименту над задачкой пункта (1) посвящена другая ветка форума:
http://www.rsdn.ru/Forum/Message.aspx?mid=897841&only=1
Автор: Сергей Губанов
Дата: 13.11.04

http://www.rsdn.ru/Forum/Message.aspx?mid=897965&only=1
Автор: Сергей Губанов
Дата: 14.11.04

http://www.rsdn.ru/Forum/Message.aspx?mid=898825&only=1
Автор: Сергей Губанов
Дата: 15.11.04


Сдесь же, давайте рассмотрим пункт (2).

Исходный код программы на языке C#:
namespace TestGC2
{
    class Object
    {
        public byte[] data;
    }

    class Program
    {
        static void f(int N)
        {
            Object[] arr = new Object[N];
            for (int n = 1; n < 100*N; n++)
            {
                Object x = new Object();
                x.data = new byte[1000];
                arr[n % N] = x;
            }
        }

        static void Main(string[] args)
        {
            const int N = 100000;
            long t;
            //-----------------------------------//
            t = System.Environment.TickCount;
            f(N);
            t = System.Environment.TickCount - t;
            //-----------------------------------//
            System.Console.WriteLine("N = {0}, t = {1}", N, t);
            System.Console.ReadLine();
        }
    }
}

Эта программа создает N = 100'000 объектов. Каждый объект занимает в памяти дополнительно 1'000 байтов (смотри строчку: x.data = new byte[1000]; ). Стало быть, суммарный объем полученной динамической структуры можно оценить в N*1000 = 100 мегабайтов + незначительные накладные расходы связанные с RTTI информацией хранимой в объектах (число в 100 мегабайтов обусловлено еще и тем, что на компьютере на котором я проводил эксперимент свободно как раз около 100-150 мегабайтов оперативной памяти). Массив заполняется 100 раз: for(int n = 1; n < 100*N; n++), стало быть, поскольку оперативной памяти хватает лишь только на 1-1.5 таких массива (а не на 100 массивов), то сборщик мусора должен запускаться несколько десятков раз. В идеале, если бы сборщик мусора был бы супер умным, то он должен был бы использовать не всю доступную память, а лишь только столько памяти сколько необходимо для N+1 объектов.
Язык   : C#
Железо : процессор Athlon 1.47Mhz, память 256Mb DDR266
Софт   : Windows XP SP2, MS Visual Studio .NET 2003, MS .NET Framework 1.1
Время выполнения программы = 163 секунды, т. е. 2 минуты 43 секунды.


Аналогичную программу я написал на языке Component Pascal
MODULE TestGC2;

IMPORT StdLog, Services;

PROCEDURE f(N: INTEGER);
TYPE Data    = POINTER TO ARRAY OF BYTE;
     Object  = POINTER TO RECORD data: Data END;
     Objects = POINTER TO ARRAY OF Object;
VAR arr: Objects; x: Object; n: INTEGER;
BEGIN
  NEW(arr, N);
  FOR n := 1 TO 100*N DO NEW(x); NEW(x.data, 1000); arr[n MOD N] := x END
END f;

PROCEDURE Test* ();
CONST N = 100000;
VAR t: LONGINT;
BEGIN
  t := Services.Ticks();
  f(N);
  t := Services.Ticks() - t;
  StdLog.String("N ="); StdLog.Int(N);
  StdLog.String(", t ="); StdLog.Int(t); StdLog.String(" ms");
  StdLog.Ln();
END Test; 
    
END TestGC2.

Собственно, первоначально, я сделал данные объекта не динамическим массивом, а статическим, то есть было так:
  Object  = POINTER TO RECORD 
      data: ARRAY 1000 OF BYTE;
    END;

но потом мне пришлось заменить статический массив на динамический, так как в языке C#, на сколько я его знаю, статических массивов не бывает, и, стало быть, я не смогу написать эквивалентную программу. Может быть я ошибаюсь? Есть в C# статические массивы или нет? Ну ладно, пока пусть будет динамический...
Язык   : Component Pascal
Железо : <тоже самое>
Софт   : <тоже самое> + Oberon Microsystems, Inc BlackBox 1.4
Время выполнения программы = 50 секунд.
Когда я использовал в качестве data статический массив байтов, то время выполнения составляло 38 секунд.

И .NET-товский и BlackBox-совский сборщики мусора не порадовали экономным использованием памяти (всего для N+1 объектов), и тот и другой захватывали максимальное доступное на компьютере количество оперативной памяти (150 мегабайтов).



В данном конкретном тесте, .NET-товский сборщик мусора работал в 163/50 = 3.26 раза медленнее чем BlackBox-совский сборщик мусора. Если вспомнить про задачку с N^2 ссылками между N объектами, то там тоже .NET-товский сборщик мусора работал более чем в 3 раза медленнее чем BlackBox-совский сборщик мусора.
 N    кол.связей   один объект  вся структура   BlackBox   .NET        Отношение времен .NET/BlackBox
                                                                    
2000    4 млн         8 Kb        16 Mb          0.23 сек    0.83 сек    3.60 раз
3000    9 млн        12 Kb        36 Mb          0.55 сек    2.27 сек    4.13 раз
4000   16 млн        16 Kb        64 Mb          1.17 сек    3.99 сек    3.41 раз
5000   25 млн        20 Kb       100 Mb          1.70 сек    6.37 сек    3.75 раз
6000   36 млн        24 Kb       144 Mb          2.67 сек   10.03 сек    3.76 раз
7000   49 млн        28 Kb       196 Mb          4.17 сек   18.12 сек    4.35 раз
8000   64 млн        32 Kb       256 Mb          4.12 сек   22.22 сек    5.39 раз

И что же это такое получается? Вот уже на двух совершенно разных задачах .NET-товский сборщик мусора работает более чем в 3 раза медленнее чем BlackBox-совский сборщик мусора. Как это объяснить? Может быть в следующей версии .NET сборщик мусора станет более шустрым?
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.