Оптимизация итераций большого набора данных C # - внешний код в профилировщике и странное поведение - PullRequest
0 голосов
/ 01 сентября 2018

Текущая задача, перебирающая массивные словари, вызывает у меня головную боль. Я не могу точно определить точный источник высокой загрузки ЦП, поэтому надеюсь, что некоторые гуру C # здесь могут дать мне несколько советов и подсказок.

Установка состоит из 10 предварительно выделенных словарей Guid-byte [], каждый из которых содержит миллион записей. Процесс перебирает все из них, каждый словарь имеет свой собственный поток. Простая итерация по всем из них и передача ссылки byte [] на делегат итерации, приводящий к случайному результату, занимает менее 2 мс, но фактический доступ к любому байту в содержащихся записях приводит к увеличению этого числа до 300 + мс.

Примечание: делегат итерации создается перед любыми итерациями, и тогда я только передаю ссылку.

Если я ничего не делаю с полученной байтовой ссылкой, все это невероятно быстро:

            var iterationDelegate = new Action<byte[]>((bytes) =>
            {
                var x = 5 + 10;
            });

Но как только я пытаюсь получить доступ к самому первому байту (который фактически содержит указатель на метаданные строки где-то еще)

            var iterationDelegate = new Action<byte[]>((bytes) =>
            {
                var b = (int)bytes[0];
            });

Общее время увеличивается, и что еще более странно, первый набор итераций занимает 30 мс, второй 40+, третий 100+ и четвертый может занять 500 мс + ... затем я прекращаю тестировать производительность, Отключаю вызывающий поток в течение нескольких секунд, и как только я начинаю повторять итерацию, она начинается случайно через 30 мс, а затем поднимается так же, как и раньше, пока я снова не дам ей «время дышать».

Когда я смотрю его в дереве вызовов VS-процессора, 93% ЦП потребляется [External Code], который я не могу просмотреть или хотя бы увидеть, что это такое.

Могу ли я чем-нибудь помочь? У ГК тяжелые времена?

Редактировать 1: Фактический код, который я хочу запустить:

            var iterationDelegate = new Action<byte[]>((data) =>
            {
                //compare two bytes, ensure the row belongs to desired table
                if (data[0] != table.TableIndex)
                    return;

                //get header length
                var headerLength = (int)data[1];

                //process the header info and retrieve the desired column data position:

                var columnInfoPos = (key * 6) + 2;

                var pointers = new int[3] {
                    //data position
                BitConverter.ToInt32(new byte[4] {
                    data[columnInfoPos],
                    data[columnInfoPos + 1],
                    data[columnInfoPos + 2],
                    data[columnInfoPos + 3] }),
                    //data length
                BitConverter.ToUInt16(new byte[2] {
                    data[columnInfoPos + 4],
                    data[columnInfoPos + 5] }),
                //column info position
                columnInfoPos };


            });

Но этот код еще медленнее, время итерации ~ 150, ~ 300, ~ 600, 700 +

Это рабочий класс, который поддерживается для каждого магазина в соответствующих потоках:

            class PartitionWorker
            {
                private ManualResetEvent waitHandle = new ManualResetEvent(true);
                private object key = new object();
                private bool stop = false;
                private List<Action> queue = new List<Action>();

                public void AddTask(Action task)
                {
                    lock (key)
                        queue.Add(task);
                    waitHandle.Set();
                }

                public void Run()
                {
                    while (!stop)
                    {
                        lock (key)
                            if (queue.Count > 0)
                            {
                                var task = queue[0];
                                task();
                                queue.Remove(task);
                                continue;
                            }
                        waitHandle.Reset();
                        waitHandle.WaitOne();
                    }
                }

                public void Stop()
                {
                    stop = true;
                }
            }

И, наконец, код, который запускает итерации, этот код запускается из Задачи для каждого входящего запроса TCP.

            for (var memoryPartition = 0; memoryPartition < partitions; memoryPartition++)
            {
                var memIndex = memoryPartition;
                mem[memIndex].AddJob(() =>
                {
                    try
                    {
                        //... to keep it shor i have excluded readlock and try/finally
                        foreach (var obj in mem[memIndex].innerCache.Values)
                        {
                            iterationDelegate(obj.bytes);
                        }
                        //release readlock in finally..
                    }
                    catch
                    {

                    }
                    finally
                    {
                        latch.Signal();
                    }
                });
            }
            try
            {
                latch.Wait(50);
                sw.Stop();
                Console.WriteLine("Found " + result.Count + " in " + sw.Elapsed.TotalMilliseconds + "ms");
            }
            catch
            {
                Console.WriteLine(">50");
            }

Edit2: Словари предварительно распределяются с помощью

private Dictionary<Guid, byte[]> innerCache = new Dictionary<Guid, byte[]>(part_max_entries);

и в отношении записей они составляют в среднем 70 байт. Процесс занимает около 2 ГБ памяти с 10 000 000 записей, разделенных на 10 словарей.

Структура записи следующая:

T | HL | {POS | POS | POS | POS | ЛЕН | LEN} | {байты данных}

где | указывает отдельные байты

  • T - байтовый указатель на словарь метаданных таблицы
  • HL - длина байта части заголовка, если запись

POS и LEN повторяются для каждого значения данных в записи:

  • POSx4 = int, указывающий положение этих данных в записи
  • POSx2 = короткая длина этих данных в записи

, а затем {байты данных} являются полезными данными

1 Ответ

0 голосов
/ 06 сентября 2018

Для тех, кому может быть интересно, наибольший прирост производительности был на самом деле использовать горячее вращение вместо сна / задержки / WaitHandles. Удар процессора незначителен даже при большом количестве параллельных запросов. Для очень интенсивных операций Реализован запасной вариант: если вращение занимает более 3 мс, оно возвращается к ожиданию потока. Код теперь работает с довольно постоянными записями 24ms / 10mil. Также было полезно удалить все коллекции GC из кода и использовать как можно больше переменных.

Вот код счетчика, который я использую:

    private static void spin(ref Stopwatch sw, double spinSeconds)
    {
        sw.Start();
        while (sw.ElapsedTicks < spinSeconds) { }
        sw.Stop();
    }

Примечание: это может использоваться только с кодом, который выполняется в его собственном потоке! Если вы используете его в однопоточном приложении, вы заблокируете весь свой код здесь.

Edit: Также стоит отметить, что по какой-то причине перезапись цикла for таким образом, чтобы он считал 0, оказала значительное влияние на производительность. Я не знаю точную механику, почему, но я предполагаю, что сравнение с нулем просто быстрее.

Я также изменил словарь, теперь это словарь (Guid, Int). Я добавил массив byte [] [] и словарь int указывает на индекс в этом массиве. Итерация по этому массиву намного быстрее, чем перечисление элементов словаря и итерации по ним. Хотя есть механизмы, которые мне нужно было реализовать для обеспечения согласованности.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...