пятница, 30 сентября 2011 г.

Бенчмарк оперативной памяти

Не люблю различные бенчмарки типа SiSoft Sandra и подобные за то, что у них у разных версий на одном и том же компьютере разные показатели. Как правило, у новых версий показатели выше. На одном и том же компьютере. Это ставит под сомнение фактическую ценность таких бенчмарков как источников каких-то абсолютных показателей. Да и и их ценность при сравнительном тестировании тоже весьма сомнительна - ну кто знает, что они там имеют ввиду под своими попугаями? Вот функция memcpy(), например, проста для понимания и широко используется и поэтому именно она может быть использована для измерения скорости обмена между процессором и ОЗУ. Набросал простой бенчмарк:

#include <QtCore/QCoreApplication>
#include <vector>
#include <iostream>
#include <QTime>
#include <QDebug>
#include <QThread>
 
class MemReadTestThread : public QThread {
const unsigned char * test_memory_block;
const size_t test_memory_size;
const size_t repeat_count;
const size_t buf_size;
public:
    MemReadTestThread(const unsigned char * block, const size_t size, const size_t bufsize, const size_t repeat_cnt)
        : QThread(), test_memory_block(block), test_memory_size(size), repeat_count(repeat_cnt), buf_size(bufsize)
    {
    }
    void run() {
        setPriority(HighPriority);
        std::vector<unsigned char> buf(buf_size);
 
        for(size_t n = 0; n < repeat_count; n++) {
            size_t i = 0;
            while(i + buf_size < test_memory_size) {
                memcpy(&buf[0], &test_memory_block[i], buf_size);
                i += buf_size;
            }
        }
    }
};
 
class MemWriteTestThread : public QThread {
unsigned char * test_memory_block;
const size_t test_memory_size;
const size_t repeat_count;
const size_t buf_size;
public:
    MemWriteTestThread(unsigned char * block, const size_t size, const size_t bufsize, const size_t repeat_cnt)
        : QThread(), test_memory_block(block), test_memory_size(size), buf_size(bufsize), repeat_count(repeat_cnt)
    {
    }
    void run() {
        setPriority(HighPriority);
        std::vector<unsigned char> buf(buf_size);
 
        for(size_t n = 0; n < repeat_count; n++) {
            size_t i = 0;
            while(i + buf_size < test_memory_size) {
                memcpy(&test_memory_block[i], &buf[0], buf_size);
                i += buf_size;
            }
        }
    }
};
 
int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    using namespace std;
    const size_t size = 768*1024*1024;
    const size_t bufsize = 32*1024;
    vector<unsigned char> bytes1(size), bytes2(size);
    
    //засекаем время
    QTime time;
    time.restart();
    const size_t count = 10; 
    //создаем и запускаем 2 потока чтения из памяти
    MemReadTestThread rt1(&bytes1[0], bytes1.size(), bufsize, count);
    MemReadTestThread rt2(&bytes2[0], bytes2.size(), bufsize, count);
    rt1.start();
    rt2.start();
    //ждем, пока оба не завершатся
    rt1.wait();
    rt2.wait();
    //узнаем время выполнения и считаем результат
    float secs = (float) time.elapsed()/1000;
    size_t total_megabytes_copied = ((bytes1.size()+bytes2.size())/(1024*1024))*count;
    qDebug() << "********** memory read ************";
    qDebug() << "time = " << secs << " sec, copied size = " << total_megabytes_copied << " megabytes";
    float megbytes_per_sec = (float)total_megabytes_copied/secs;
    qDebug() << megbytes_per_sec << " megabytes per sec";
 
    //засекаем время
    time.restart();
    //создаем и запускаем два потока записи в память
    MemWriteTestThread wt1(&bytes1[0], bytes1.size(), bufsize, count);
    MemWriteTestThread wt2(&bytes2[0], bytes2.size(), bufsize, count);
    wt1.start();
    wt2.start();
    //ждем, пока не завершатся
    wt1.wait();
    wt2.wait();
    //узнаем время и вычисляем результат 
    secs = (float) time.elapsed()/1000;
    total_megabytes_copied = ((bytes1.size()+bytes2.size())/(1024*1024))*count;
    qDebug() << "********** memory write ************";
    qDebug() << "time = " << secs << " sec, copied size = " << total_megabytes_copied << " megabytes";
    megbytes_per_sec = (float)total_megabytes_copied/secs;
    qDebug() << megbytes_per_sec << " megabytes per sec";
 
    return a.exec();
}

Комментировать особо нечего, кроме того, почему нам нужно тестировать в двух потоках. Тут все очень интересно. Как всем известно, в современных контроллерах памяти, если есть две одинаковые (по объему и скорости) планки памяти, то есть возможность использовать двухканальный режим памяти. То есть две 64-битные планки памяти (а они имеют именно такую разрядность) как бы превращаются в одну большую 128-битную планку памяти. Соответственно,  скорость работы увеличивается в 2 раза. Этот режим называется dual ganged mode. А есть еще dual unganged mode - это тоже режим с удвоенной пропускной способностью, но контроллер памяти обращается к памяти не как к одной 128-битной планке, а как к двум планкам, каждая из которых имеет свою собственную независимую 64-битную шину. То есть можно в одну планку писать и одновременно что-то читать с другой планки памяти. При этом удвоенная скорость будет только в том случае, если тестировать две планки памяти независимо друг от друга, то есть в разных потоках. Чтобы убедиться в этом, можно эту програмку сделать однопоточной и увидеть воочию двукратное падение скорости. Разумеется, это относится только к unganged mode, т.к. ganged mode совершенно все равно, сколько потоков будет запущено - на результат это не повлияет. Трудность заключается в том, что нет уверенности, что один тестируемый массив полностью расположен в одной планке, а второй - полностью в другой планке и это может негативно повлиять на итоговый результат. С другой стороны, в реальных приложениях вряд ли кто-то будет пытаться расположить массивы данных так, чтобы они оптимальным образом были рассредоточены по двум планкам памяти, поэтому результаты работы данного простого бенчмарка, в принципе, ближе к реальности, чем в идеальном случае. А результаты у меня такие:


Это материнская плата MSI 760GM-P33 на чипсете 760G и процессор AMD X4 640 и две планки памяти DDR3-1333 по 2 гигабайта.

понедельник, 6 июня 2011 г.

Работа с регулярными выражениями в std::tr1

TR1 - крайне полезное расширение STL, и, что самое важное, есть во многих компиляторах.

Допустим, нам нужно распарсить строку с диапазонами страниц для печати, разделенными запятыми. Строка типа "1, 2, 4-7, 10". Первое, что пришло в голову:
using namespace std;
string str = "  1 , 2, 4 , 3 - 7 , 9 - 11";
//отдельная цифра может быть окружена пробелами со всех сторон
const string numb = "[\\s]*[0-9]+[\\s]*";
//диапазон -это либо одна цифра, либо две цифрыЮ разделенные тире
const string diapason = numb + "(\\-" + numb + "){0,1}";
const std::tr1::regex regex_diap(diapason);
//выводим все подстроки, соответствующие паттерну
const tr1::sregex_token_iterator end;
tr1::sregex_token_iterator iter = tr1::sregex_token_iterator(str.begin(), str.end(), regex_diap);
for (; iter != end; ++iter)
    std::cout << *iter << std::endl;
В результате программа выведет:
1
2
4
7 - 3
8 - 11

Все хорошо до тех пор, пока мы не попробуем вместо правильной строки ввести, например, такую неправильную строку: " 1 , 2, 4 , 3aaa - 7 , 9 - 11". В этом случае программа выведет следующее:
1
2
4
3
7
9 - 11

То есть программа распознала неправильный диапазон, как два отдельных числа. Нас такой результат не устраивает. Значит, надо указать, что диапазон должен содержать только цифры, пробелы и минус между цифрами. Порывшись в гугле, нашел такой вариант решения:
using namespace std;
//входная строка
string str = "  1 , 2, 4 , 5 - 7, 9 - 11";
//целое число, которое может быть обрамлено пробельными символами
const string numb = "[\\s]*[0-9]+[\\s]*";
//вот тут мы используем т.н. просмотр вперёд и назад
const string one_page = "(?<=(,|^))" + numb + "(?=(,|$))";
const string many_page = "(?<=(,|^))" + numb + "\\-" + numb + "(?=(,|$))";
//число или диапазон
const string one_many_page = "(" + one_page + "|" + many_page + ")";
const tr1::regex rx(one_many_page);
const tr1::sregex_token_iterator end;
tr1::sregex_token_iterator it;
for (it = tr1::sregex_token_iterator(str.begin(), str.end(), rx); it != end; ++it)
    std::cout << *it << std::endl;
Самое интересное тут - это т.н. просмотр вперед и назад без захвата выражения. Подробнее - в википедии. Благодаря чему мы указываем, что любому отдельному числу должна предшествовать либо запятая, либо начало строки, отделенные нулем или большим числом пробелов и за ним должна следовать либо запятая, либо конец строки, отделенные нулем или большим числом пробелов:
const string numb = "[\\s]*[0-9]+[\\s]*";
const string one_page = "(?<=(,|^))" + numb + "(?=(,|$))";
 
Аналогично с диапазоном чисел:
const string numb = "[\\s]*[0-9]+[\\s]*";
сonst string many_page = "(?<=(,|^))" + numb + "\\-" + numb + "(?=(,|$))";
 
Ну и регулярное выражение, которое выбирает ИЛИ число ИЛИ диапазон:
const string one_many_page = "(" + one_page + "|" + many_page + ")";
 
Теперь, если указать неверный диапазон чисел типа " 1 , 2, 4 , 7aaa - 3, 8 - 11", то на выходе получим:
1
2
4
8 - 11

То есть неверный диапазон мы просто пропустили, вместо неверной его интерпретации, что и требовалось.

Для подсветки синтаксиса использовал highlight.hohli.com

среда, 25 мая 2011 г.

"Черная магия" гугловских программистов

На хабре наткнулся на пост об безопасном определении числа элементов массива.
Суть состоит в следующем. Например, большинство программистов для определения количества элементов в массиве используют следующую конструкцию:
int XX[] = { 1, 2, 3, 4 };
size_t N = sizeof(XX) / sizeof(XX[0]);
Обычно это оформляют в макрос следующего вида:
#define count_of(arg) (sizeof(arg) / sizeof(arg[0]))
Однако он может послужить причиной ошибки, так как ему случайно можно подсунуть простой указатель, и он не будет возражать по этому поводу:
void Test(int C[3])
{
  int A[3];
  int *B = Foo();
  size_t x = count_of( A ); // Ok
  x = count_of( B ); // Error
  x = count_of( C ); // Error
}
Конструкция count_of(A) работает корректно и возвращает количество элементов в массиве A, равное трём. Но если случайно применить count_of() к указателю, то результатом будет малоосмысленное значение.
В проекте Chromium гугловские программисты использовали такой макрос, который позволяет избежать описанных выше ошибок. Вот этот магический макрос:
template <typename T, size_t N>
char (&ArraySizeHelper(T (&array)[N]))[N];
#define arraysize(array) (sizeof(ArraySizeHelper(array))) 
Объяснено на хабре было плохо (ну для меня по крайней мере), потом нашел другое объяснение, где уже было немного понятнее.
К моему позору, я начисто забыл, как в С++ выглядит прототип функции, возвращающей ссылку на массив. А выглядит он, например, так:
int (&get_array())[10];
Это прототип функции, возвращающей ссылку на массив из 10 интов. Такой способ определения ссылки на массив из N элементов - это наследие C. Выглядит точно так же, как и определение указателя на массив, достаточно "&" заменить на "*".
Далее все очень просто:
template <typename T, size_t N> char (&ArraySizeHelper(T (&array)[N]))[N];
- это шаблонизированный прототип функции, принимающей ссылку на массив произвольного типа T из N элементов и возвращающий ссылку на массив типа char из N элементов. Очевидно, что char потому, что число элементов в массиве в этом случае равно количеству байт, занимаемым массовом. Смысл шаблона функции именно в том, что размер массива "магически" передается сразу на выход функции, фактически определяя тип возвращаемого значения (а конкретно размер) во время компиляции. То есть, фактически, саму функцию определять не надо - достаточно только ее прототипа. А он будет сгененерирован во время компиляции. То, что функция реально не вызывается, вызвало у меня некое недопонимание. Однако именно так дело и обстоит. Такова особенность работы функции sizeof() - ей достаточно знать только тип вычисляемого выражения. А он известен на этапе компиляции. То есть нет необходимости вызывать саму функцию. А значит, нет необходимости и ее реализовывать - достаточно только объявления. Почитав немного Александреску, такие фокусы воспринимать уже гораздо легче и привычнее.Чтобы было понятнее, лучше всего привести пару примеров. У нас определен такой массив:

const int arr_size = 10;
const int int_arr[arr_size]; 
 
Теперь, если раскрыть шаблон ArraySizeHelper для него, то мы получим такое определение функции:

char (&ArraySizeHelper(int (&array)[10]))[10]; 
 
То есть прототип функции, получающей параметром ссылку на массив из 10 целых чисел и возвращающую ссылку на массив из 10 char. Естественно, что sizeof() для массива из 10 элементов типа char вернет 10.
 
Теперь, при попытке подсунуть что-то не то будет ошибка компиляции, что позволит выявить ошибку:
template <typename T, size_t N>
char (&ArraySizeHelper(T (&array)[N]))[N];
#define arraysize(array) (sizeof(ArraySizeHelper(array)))
 
void Test(int C[3])
{
  int A[3];
  int *B = Foo();
  size_t x = arraysize( A ); // Ok
  x = arraysize( B ); // Ошибка компиляции
  x = arraysize( C ); // Ошибка компиляции
}


P.S.
Для подсветки синтаксиса использовал highlight.hohli.com