Проблемы с производительностью при масштабировании оператора MSVC 2005 << в разных потоках - PullRequest
2 голосов
/ 01 сентября 2009

Просматривая некоторые из наших журналов, я заметил в профилировщике, что мы тратили много времени в форматах operator<< и т. Д. Похоже, что есть общая блокировка, которая используется всякий раз, когда ostream::operator<< вызывается при форматировании int (и, вероятно, удваивается). После дальнейшего изучения я сузил его до этого примера:

Loop1, который использует ostringstream для форматирования:

DWORD WINAPI doWork1(void* param)
{
    int nTimes = *static_cast<int*>(param);
    for (int i = 0; i < nTimes; ++i)
    {
        ostringstream out;
        out << "[0";
        for (int j = 1; j < 100; ++j)
            out << ", " << j; 
        out << "]\n";
    }
    return 0;
}

Loop2, который использует тот же ostringstream для всего, кроме формата int, что делается с itoa:

DWORD WINAPI doWork2(void* param)
{
    int nTimes = *static_cast<int*>(param);
    for (int i = 0; i < nTimes; ++i)
    {
        ostringstream out;
        char buffer[13];
        out << "[0";
        for (int j = 1; j < 100; ++j)
        {
            _itoa_s(j, buffer, 10);
            out << ", " << buffer;
        }
        out << "]\n";
    }
    return 0;
}

Для моего теста я запускал каждый цикл несколько раз с 1, 2, 3 и 4 потоками (у меня 4-х ядерный компьютер). Количество испытаний является постоянным. Вот вывод:

doWork1: all ostringstream
n       Total
1         557
2        8092
3       15916
4       15501

doWork2: use itoa
n       Total
1         200
2         112
3         100
4         105

Как видите, производительность при использовании ostringstream ужасна. При добавлении потоков становится в 30 раз хуже, тогда как в Itoa это происходит в 2 раза быстрее.

Одной из идей является использование _configthreadlocale(_ENABLE_PER_THREAD_LOCALE) в соответствии с рекомендацией M $ в этой статье . Это не помогает мне. Вот еще один пользователь , у которого, похоже, возникла похожая проблема.

Мы должны иметь возможность форматировать целые числа в нескольких потоках, работающих параллельно для нашего приложения. Учитывая эту проблему, нам нужно либо выяснить, как это сделать, либо найти другое решение для форматирования. Я могу написать простой класс с оператором <<, перегруженным для целочисленного и плавающего типов, а затем иметь шаблонную версию, которая просто вызывает оператор << в базовом потоке. Немного некрасиво, но я думаю, что смогу заставить это работать, хотя, возможно, не для определенного пользователем <code>operator<<(ostream&,T), потому что это не ostream.

Я также должен прояснить, что это создается с помощью Microsoft Visual Studio 2005. И я считаю, что это ограничение связано с их реализацией стандартной библиотеки.

Ответы [ 3 ]

1 голос
/ 01 сентября 2009

Если в стандартной реализации библиотеки Visual Studio 2005 есть ошибки, почему бы не попробовать другие реализации? Как:

или даже Dinkumware , на котором основана стандартная библиотека Visual Studio 2005, возможно, решали проблему с 2005 года.

Редактировать : Другой упомянутый вами пользователь использовал Visual Studio 2008 с пакетом обновления 1 (SP1), что означает, что, вероятно, Dinkumware не устранил эту проблему.

1 голос
/ 02 сентября 2009

Проблема может быть в выделении памяти. malloc, который использует «new», имеет внутреннюю блокировку. Вы можете увидеть это, если вступите в это. Попробуйте использовать локальный распределитель потоков и посмотрите, исчезает ли плохая производительность.

1 голос
/ 01 сентября 2009

Меня не удивляет, MS установила «глобальные» блокировки для нескольких общих ресурсов - самой большой головной болью для нас была блокировка памяти BSTR несколько лет назад.

Лучшее, что вы можете сделать, это скопировать код и заменить блокировку ostream и разделяемую память преобразования своим собственным классом. Я сделал это, когда я записывал поток, используя систему журналирования в стиле printf (то есть мне пришлось использовать регистратор printf и обернуть его с помощью моих операторов потока). После того, как вы скомпилировали это в свое приложение, вы должны быть такими же быстрыми, как в Itoa. Когда я буду в офисе, я возьму код и вставлю его для вас.

EDIT: как и было обещано:

CLogger& operator<<(long l)
{
    if (m_LoggingLevel < m_levelFilter)
        return *this;

    // 33 is the max length of data returned from _ltot
    resize(33);

    _ltot(l, buffer+m_length, m_base);
    m_length += (long)_tcslen(buffer+m_length);

    return *this;
};

static CLogger& hex(CLogger& c)
{
    c.m_base = 16;
    return c;
};

void resize(long extra)
{
    if (extra + m_length > m_size)
    {
        // resize buffer to fit.
        TCHAR* old_buffer = buffer;
        m_size += extra;
        buffer = (TCHAR*)malloc(m_size*sizeof(TCHAR));
        _tcsncpy(buffer, old_buffer, m_length+1);
        free(old_buffer);
    }
}

static CLogger& endl(CLogger& c)
{
    if (c.m_length == 0 && c.m_LoggingLevel < c.m_levelFilter)
        return c;

    c.Write();
    return c;
};

Извините, я не могу позволить вам иметь все это, но эти 3 метода показывают основы - я выделяю буфер, изменяю его размер, если необходимо (m_size - размер буфера, m_length - текущая длина текста) и сохраняю его в течение всего времени объекта регистрации. Содержимое буфера записывается в файл (или OutputDebugString, или список) в методе endl. У меня также есть уровень журналирования, чтобы ограничить вывод во время выполнения. Таким образом, вы просто заменяете свои вызовы в ostringstream этим, а метод Write () перекачивает буфер в файл и очищает длину. Надеюсь, это поможет.

...