О библиотеке сжатия zlib

Сегодня я хочу рассказать о замечательной библиотеке zlib.

Мне, как программисту игрушек, раньше приходилось решать проблему быстрой распаковки ресурсов при загрузке игровых уровней. Самое неприятное, что может принести игра пользователю, кроме падения и ошибок, это заставить его долго ожидать загрузки уровня (хотя 10-15 лет назад загрузка одного уровня по 5-10 минут было совершенно нормальным явлением).

Впервые в жизни мне пришлось столкнуться с компрессией данных в игре «Петрович и все, все, все», которую мы делали в Trickster Games для Андрея Бильжо.

Все изображения в игре используют от двух до десяти цветов (а музыка восемь нот, ага). Вот пример:

Петрович и инопланетянин

На этом скриншоте присутствуют: Петрович (два цвета), Инопланетянин aka «логотип Trickster Games» (2 цвета), фон (4 цвета). Каждое изображение может занимать от 256 килобайт до 4-х мегабайт (потому что DDS). Загрузка одного уровня, состоявшего из 50-200 таких изображений, занимала от 10 до 45 секунд. Разумеется, это нас в корне не устраивало.

После мозгового штурма на эту тему было найдено решение — использовать алгоритм RLE. Этот алгоритм позволяет очень эффективно сжимать длинную цепочку одинаковых бит, что наглядно показано в википедии: блок данных «AAAAAAAAAAAAAAABBBBBC» мы записываем вот так «15A5BC». Экономия, как говорится, налицо.

Интерфейс для работы с этим алгоритмом был выбран очень простой. Как оказалось впоследствии, такой интерфейс с мелкими различиями предоставляют практически все библиотеки сжатия без потерь.

/* Сжимает данные из буфера src в dst.
 * src - входной буфер
 * srcSize - размер входного буфера в байтах
 * dst - выходной буфер
 * dstSize - перед вызовом должен быть установлен в размер буфера dst, 
 *     после вызова туда записывается реальное количество сжатых байт
 *
 * Функция возвращает true, если данные были сжаты успешно 
 * (это значит, что уровень сжатия меньше или равен единице)
 */
bool compressRLE( void *dst, int *dstSize, const void *src, int srcSize);
 
/* Расжимает данные из буфера src в dst.
 * src - входной буфер
 * srcSize - размер входного буфера в байтах
 * dst - выходной буфер
 * dstSize - перед вызовом должен быть установлен в размер буфера dst, 
 *     после вызова туда записывается реальное количество распакованных байт
 *
 * Функция возвращает true, если данные были расжаты успешно 
 */
bool uncompressRLE( void *dst, int *dstSize, const void *src, int dstSize);

Разумеется, я не могу показать нашу реализацию алгоритма без разрешения начальства, но, думаю, вы справитесь сами. Коэффициент сжатия на наших данных составлял где-то 0.01 процента (то есть тысяча байт сжимались в десять).

..И тут мы плавно переходим к zlib.

Библиотека zlib используется практически везде, где есть CPU: мобильные телефоны, компьютеры, микроволновки, современные автомобили, телевизоры — список можно продолжать бесконечно. Кто знает, может она сейчас трудится на Марсе внутри ровера Curiosity, сжимая, передаваемые на Землю, данные.

Библиотека может сжимать блок данных или поток. Рассмотрим вкратце сжатие блока данных. Описание метода вот такое:

// Сжимает данные методом deflate из буфера src в dst
// dest - выходной буфер
// destLen - размер выходного буфера (после удачного сжатия 
//     сюда сохраняется размер сжатых данных)
// source - входной буфер
// sourceLen - размер входного буфера
// Возвращает Z_OK в случае удачи, записывает размер сжатых данных в destLen
ZEXTERN int ZEXPORT compress OF((Bytef *dest,   uLongf *destLen,
                                 const Bytef *source, uLong sourceLen));

Пример вызова этой функции. Он ничего полезного не делает, просто для наглядности.

#include <zlib.h>
#include <memory.h>
#include <stdio.h>
 
#define SRC_SIZE 512
#define DST_SIZE 512
 
int main()
{
    // Входной буфер размером SRC_SIZE
    unsigned char source[SRC_SIZE];
    // Тут мы будем хранить размер входного буфера 
    // (переменная просто для наглядности)
    const unsigned long sourceLen = SRC_SIZE;
    // Выходной буфер
    unsigned char dest[DST_SIZE];
    // Размер выходного буфера
    unsigned long destLen = DST_SIZE;
 
    // Будем сжимать буфер забитый нулями
    memset(source, 0, SRC_SIZE * sizeof(source[0]));
 
    if (Z_OK == compress(dest, &destLen, source, sourceLen))
    {
        // Все успешно
        printf("Compress ratio: %.2f\n", destLen/(float) sourceLen);
    }
    else
    {
        // Тут можете разобрать код ошибки и вывести более подробную информацию
        printf("Compress failed\n");
    }
 
    return 0;
}

Пример сжатия потока данных вы можете посмотреть в маленькой утилитке zpipe.

Есть вопросы? Спрашивайте, не стесняйтесь.

P.S. Если вы дочитали досюда, рекомендую изучить libdeflate.

Тестовые задания для программистов. Издание исправленное и дополненное

Я просто оставлю это здесь.

[user@domain ~]# cc -Wall -Werror -pedantic Binary.c
BinaryPrint.c:6:5: error: C++ style comments are not allowed in ISO C90
BinaryPrint.c:6:5: error: (this will be reported only once per input file)
cc1: warnings being treated as errors
BinaryPrint.c: In function 'BinaryPrint':
BinaryPrint.c:13: warning: ISO C90 forbids mixed declarations and code

diff -r afee20b80dff BinaryPrint.c
--- a/BinaryPrint.c     Tue Feb 22 09:11:12 2011 +0300
+++ b/BinaryPrint.c     Tue Nov 15 18:51:10 2011 +0300
@@ -3,31 +3,31 @@
 
 void BinaryPrint( long n )
 {
-    // Частный случай - n равно нулю
+    unsigned long mask = 1 << (sizeof(n) * 8 - 1);
+
+    /* Пропускаем бесполезные нули слева */
+    int skipZero = 1;
+
+    /* Частный случай - n равно нулю */
     if (n == 0)
     {
         printf("0\n");
         return;
     }
 
-    unsigned long mask = 1 << (sizeof(n) * 8 - 1);
-
-    // Пропускаем бесполезные нули слева
-    int skipZero = 1;
-
     while (mask)
     {
-        // Определим текущий бит
+        /* Определим текущий бит */
         unsigned char c = (n & mask) ? 1 : 0;
 
-        // Отключим пропуск нулей в дальнейшем
+        /* Отключим пропуск нулей в дальнейшем */
         if (skipZero && c)
             skipZero = 0;
 
         if (!skipZero)
             printf("%d", c);
 
-        // Сдвинем маску на один бит вправо
+        /* Сдвинем маску на один бит вправо */
         mask >>= 1;
     }
     printf("\n");
[user@domain ~]# cc -Wall -Werror -pedantic RemoveDups.c
RemoveDups.c:8:5: error: C++ style comments are not allowed in ISO C90
RemoveDups.c:8:5: error: (this will be reported only once per input file)
cc1: warnings being treated as errors
RemoveDups.c: In function 'RemoveDups':
RemoveDups.c:16: warning: value computed is not used
RemoveDups.c:22: warning: value computed is not used
RemoveDups.c:24: warning: value computed is not used
diff -r afee20b80dff RemoveDups.c
--- a/RemoveDups.c      Tue Feb 22 09:11:12 2011 +0300
+++ b/RemoveDups.c      Tue Nov 15 18:56:00 2011 +0300
@@ -5,25 +5,25 @@
     const char * src = pStr;
     char * dst = pStr;
 
-    // Пока не дойдем до конца строки
+    /* Пока не дойдем до конца строки */
     while (*src)
     {
-        // Записываем в pStr текущий символ, если указатели разные
+        /* Записываем в pStr текущий символ, если указатели разные */
         if (dst != src)
             *dst = *src;
 
-        // Сдвигаем dst вправо
-        *dst++;
+        /* Сдвигаем dst вправо */
+        dst++;
 
-        // Пока не конец строки и следующий символ такой же
+        /* Пока не конец строки и следующий символ такой же */
         while ((*src + 1) && *(src + 1) == *src)
         {
-            // Сдвигаем указатель src вправо
-            *src++;
+            /* Сдвигаем указатель src вправо */
+            src++;
         }
-        *src++;
+        src++;
     }
-    // Запишем ноль в конец строки
+    /* Запишем ноль в конец строки */
     *dst = 0;
 }

Читайте также: «Вопросы программисту на собеседовании с ответами».

Тестовые задания для программиста

Так случилось, что в марте 2011 года я потерял работу в связи с закрытием фирмы. Взгрустнулось. Недолго думая, накатал резюме и выложил его на разные сайты: hh.ru, habrahabr.ru, job.ru и dtf.ru. В течение месяца ответило довольно много работодателей. Некоторые высылали тестовые задания для проверки. Делал я их бесплатно, в свое личное время, так что имею полное право выложить решение.

Итак, погнали: решение тестового задания от фирмы «Saber Interactive».

Задача

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

Решение

 
#include <stdio.h>
#include <limits.h>
 
void BinaryPrint( long n )
{
    // Частный случай - n равно нулю
    if (n == 0)
    {
        printf("0\n");
        return;
    }
 
    unsigned long mask = 1 << (sizeof(n) * 8 - 1);
 
    // Пропускаем бесполезные нули слева
    int skipZero = 1;
 
    while (mask)
    {
        // Определим текущий бит
        unsigned char c = (n & mask) ? 1 : 0;
 
        // Отключим пропуск нулей в дальнейшем
        if (skipZero && c)
            skipZero = 0;
 
        if (!skipZero)
            printf("%d", c);
 
        // Сдвинем маску на один бит вправо
        mask >>= 1;
    }
    printf("\n");
}
 
int main()
{
    BinaryPrint(0);
    BinaryPrint(1);
    BinaryPrint(2);
    BinaryPrint(-2);
    BinaryPrint(-1);
    BinaryPrint(LONG_MAX);
    BinaryPrint(LONG_MIN);
    return 0;
}

Задача

Напишите функцию, удаляющую последовательно дублирующиеся символы в строке: («AAA BBB ААА» -> «A B А»)
void RemoveDups (char *pStr);

Решение

#include <stdio.h>
 
void RemoveDups( char * pStr )
{
    const char * src = pStr;
    char * dst = pStr;
 
    // Пока не дойдем до конца строки
    while (*src)
    {
        // Записываем в pStr текущий символ, если указатели разные
        if (dst != src)
            *dst = *src;
 
        // Сдвигаем dst вправо
        *dst++;
 
        // Пока не конец строки и следующий символ такой же
        while ((*src + 1) && *(src + 1) == *src)
        {
            // Сдвигаем указатель src вправо
            *src++;
        }
        *src++;
    }
    // Запишем ноль в конец строки
    *dst = 0;
}
 
int main()
{
    char s[] = "AAA BBB AAA";
    RemoveDups(s);
    printf("%s\n", s);
    return 0;
}

Задача

Предположим, Вам предстоит написать простенькое программное обеспечение для учета книг в библиотеке. По некоторым, не зависящим от Вас причинам, нужно, чтобы программа была написана на С++ без использования каких-либо баз данных. Вы прикинули предварительный
список требований, которым должна удовлетворять ваша программа, и у Вас получилось:

Вам нужно хранить информацию обо всех книгах из библиотеки — название, автор, год, количество экземпляров
— Вам нужно хранить базовую информацию о читателях — ФИО, адрес, телефон
— Вам нужно уметь быстро находить читателя по ФИО или только по части ФИО
— Вам нужно хранить информацию о том, какие книги взяты какими читателями и на какой срок
— Вам нужно уметь быстро находить информацию о книгах по автору и/или названию, а также выдавать информацию о том, есть ли свободные экземпляры, и у каких читателей находятся выданные книги, и когда они будут возвращены

Разработайте структуры хранения данных, наилучшим образом подходящие для реализации этой функциональности. Постарайтесь учесть непредвиденные ситуации, которые могут возникнуть, а также организовать доступ к данным наиболее быстрым и эффективным образом. Представьте ответ в виде .h файла. Вы можете использовать классы С++, а также контейнеры STL или их аналоги.

Решение

Исходники программы на C/C++
#ifndef _LIBRARY_H_
#define _LIBRARY_H_
 
/*
 Каждой книге и каждому читателю при регистрации присваивается уникальный 
 числовой идентификатор. Доступ к книгам и читателям происходит только 
 с помощью идентификаторов.
 
 База книг/читателей преставляет собой деревья, где ключи - это идентификатор,
 а значения - книга/читатель. 
 
 Для каждого поля, по которым мы можем осуществить поиск, создается
 индексная таблица. По образу и подобию индексных таблиц в mysql.
 
 Класс Index представляет собой обертку над std::multimap. 
 std::multimap выбран потому, что значения ключей могут повторяться.
 один автор -> много книг, одно название -> много книг и т.д.
 
 Все методы поиска возвращают список идентификаторов, поскольку ключевые поля не уникальны.
*/
 
#include <string>
#include <vector>
#include <map>
#include <algorithm>
#include <assert.h>
 
// Уникальный идентификтор объекта
typedef unsigned long Ident;
// Список идентификаторов
typedef std::vector<Ident> IdentVector;
 
namespace detail
{
    // Шаблонный класс для удобства работы с multimap
    template <class T>
    class Index
    {
    public:
        void append( const T & value, Ident ident )
        {
            m_values.insert(std::make_pair(value, ident));
        }
 
        void remove( Ident ident )
        {
            for (Values::const_iterator i = m_values.begin(); i != m_values.end();)
            {
                if (i->second == ident)
                {
                    i = m_values.erase(i);
                }
                else
                {
                    ++i;
                }
            }
        }
 
        IdentVector get( const T & value ) const
        {
            IdentVector result;
 
            Values::const_iterator i = m_values.find(value);
            while (i != m_values.end() && i->first == value)
            {
                result.push_back(i->second);
                ++i;
            }
 
            return result;
        }
 
    private:
        typedef std::multimap<T, Ident> Values;
        Values m_values;
    };
 
} // namespace detail
 
// Описание книги
struct Book
{
    // Автор
    std::string author;
    // Название
    std::string title;
    // Год выпуска
    size_t year;
    // Количество экземпляров
    size_t count;
 
    Book( const std::string & author, const std::string & title, size_t year, size_t count )
        : author(author), title(title), year(year), count(count)
    {
    }        
};
 
// База данных книг
class BookDatabase
{
public:
    BookDatabase()
        : m_nextIdent(0)
    {
    }        
 
    virtual ~BookDatabase() {}
 
    // Добавление книги в библиотеку (возвращает идентификатор)
    Ident registerBook( const Book & book )
    {
        Ident ident = getNextIdent();
        m_books.insert(std::make_pair(ident, book));
        m_authors.append(book.author, ident);
        m_titles.append(book.title, ident);
        return ident;
    }
 
    // Удаление книги из библиотеки 
    void unregisterBook( Ident ident )
    {
        Books::const_iterator i = m_books.find(ident);
        if (i != m_books.end())
        {
            i = m_books.erase(i);
        }
        m_authors.remove(ident);
        m_titles.remove(ident);
    }
 
    // Получение всех идентификаторов книг с заданным авторов
    IdentVector getBooksByAuthor( const std::string & author ) const
    {
        assert(!author.empty());
        return m_authors.get(author);
    }
 
    // Получение всех идентификаторов книг с заданным названием
    IdentVector getBooksByTitle( const std::string & title ) const
    {
        assert(!title.empty());
        return m_titles.get(title);
    }
 
    // Получение всех идентикаторов книг с заданным автором и названием
    IdentVector getBooksByAuthorAndTitle( const std::string & author, const std::string & title ) const
    {
        assert(!author.empty() && !title.empty());
 
        IdentVector authors = getBooksByAuthor(author);
        IdentVector titles = getBooksByTitle(title);
 
        IdentVector result;        
        std::set_intersection(authors.begin(), authors.end(), titles.begin(), titles.end(), result.begin());
        return result;
    }
 
    // Получение данных книги по идентификатору (возвращает false, если книга не найдена)
    bool getBook( size_t ident, Book & book ) const
    {
        Books::const_iterator i = m_books.find(ident);
        if (i != m_books.end())
        {
            book = i->second;
            return true;
        }
        return false;
    }
 
    // Увеличение кол-ва книг
    void increaseBooksCount( Ident ident )
    {
        Books::iterator i = m_books.find(ident);
        if (i != m_books.end())
        {
            i->second.count++;
        }
    }
 
    // Уменьшение кол-ва книг (возвращает false, если кол-во равно нулю
    bool decreaseBooksCount( Ident ident )
    {
        Books::iterator i = m_books.find(ident);
        if (i != m_books.end())
        {
            if (i->second.count > 0)
            {
                i->second.count--;
                return true;
            }                
        }
        return false;
    }
 
    // Вернуть список всех книг, которые можно взять в библиотеке
    IdentVector getFreeBooks() const
    {
        IdentVector result;
 
        for (Books::const_iterator i = m_books.begin(); i != m_books.end(); ++i)
        {
            if (i->second.count > 0)
            {
                result.push_back(i->first);
            }
        }
 
        return result;
    }
 
private:
    Ident getNextIdent()
    {
        return m_nextIdent++;
    }
 
    // Следующий идентификатор
    Ident m_nextIdent;
 
    // Список книг
    typedef std::map<Ident, Book> Books;
    Books m_books;
 
    // Индексная таблица по авторам
    detail::Index<std::string> m_authors;
    // Индексная таблица по названиям книг
    detail::Index<std::string> m_titles;
};
 
// Читатель
struct Reader
{
    // ФИО
    std::string fullName;
    // Адрес
    std::string address;
    // Телефон
    std::string phone;
 
    Reader( const std::string & fullName, const std::string & address, const std::string & phone )
        : fullName(fullName), address(address), phone(phone)
    {
    }        
};
 
// База читателей
class ReaderDatabase
{
public:
    ReaderDatabase()
        : m_nextIdent(0)
    {
    }
 
    virtual ~ReaderDatabase() {}
 
    Ident registerReader( const Reader & reader )
    {
        Ident ident = getNextIdent();
        m_readers.insert(std::make_pair(ident, reader));
        m_fullNames.append(reader.fullName, ident);
        return ident;
    }
 
    void unregisterReader( Ident ident )
    {
        Readers::const_iterator i = m_readers.find(ident);
        if (i != m_readers.end())
        {
            m_readers.erase(i);
        }
        m_fullNames.remove(ident);
    }
 
    IdentVector getReadersByFullName( const std::string & fullName ) const
    {
        return m_fullNames.get(fullName);
    }
 
    IdentVector getReadersByPartName( const std::string & partName ) const
    {
        IdentVector result;
        for (Readers::const_iterator i = m_readers.begin(); i != m_readers.end(); ++i)
        {
            const Reader & reader = i->second;
            if (reader.fullName.find(partName) != std::string::npos)
            {
                result.push_back(i->first);
            }
        }
        return result;
    }
 
    // Информация о читателе
    bool getReader( Ident ident, Reader & reader ) const
    {
        Readers::const_iterator i = m_readers.find(ident);
        if (i != m_readers.end())
        {
            reader = i->second;
            return true;
        }
        return false;
    }
 
private:
    Ident m_nextIdent;
 
    Ident getNextIdent()
    {
        return m_nextIdent++;
    }    
 
    typedef std::map<Ident, Reader> Readers;
    Readers m_readers;
 
    detail::Index<std::string> m_fullNames;
};
 
// Библиотека
class Library : public ReaderDatabase, public BookDatabase
{
    struct Rent
    {
        Ident book;
        Ident reader;
        size_t days;
    };
 
public:
    // Взять книгу bookIdent читателем readerIdent на days дней (возвращает false в случае неудачи)
    bool popBook( Ident bookIdent, Ident readerIdent, size_t days )
    {
        if (decreaseBooksCount(bookIdent))
        {
            Rent rent;
            rent.book = bookIdent;
            rent.reader = readerIdent;
            rent.days = days;
 
            m_rents.push_back(rent);
 
            return true;
        }
        return false;
    }
 
    // Вернуть книгу bookIdent читателем readerIdent
    void pushBook( Ident bookIdent, Ident readerIdent )
    {
        for (Rents::const_iterator i = m_rents.begin(); i != m_rents.end(); ++i)
        {
            if (i->book == bookIdent && i->reader == readerIdent)
            {
                m_rents.erase(i);
                increaseBooksCount(bookIdent);
                break;
            }
        }
    }
 
    // Через сколько дней будет возвращена книга bookIdent читателем readerIdent
    size_t getRentaDays( Ident bookIdent, Ident readerIdent ) const
    {
        for (Rents::const_iterator i = m_rents.begin(); i != m_rents.end(); ++i)
        {
            if (i->book == bookIdent && i->reader == readerIdent)
            {
                return i->days;
            }
        }
    }
 
    // Возвращает список читателей, взявший данную книгу
    IdentVector getReadersByBook( Ident bookIdent ) const
    {
        IdentVector result;
        for (Rents::const_iterator i = m_rents.begin(); i != m_rents.end(); ++i)
        {
            if (i->book == bookIdent)
            {
                result.push_back(i->reader);
            }
        }
        return result;
    }
 
private:
    typedef std::vector<Rent> Rents;
    Rents m_rents;
 
};
 
#endif // _LIBRARY_H_


Если допустить, что первые два теста написаны на языке C, смотри исправления тут.

Читайте также: «Вопросы программисту на собеседовании с ответами»

Крутая система логгирования

Эта заметка была написана в моем втором по счету блоге на сайте blogspot два года назад. Сейчас я уже пришел к пониманию, что такая система логов мне не нравится. Она довольно навороченная, но все эти шаблоны меня теперь почему-то пугают. Дабы это добро не сгинуло вместе с тем дневником, публикую здесь. Прочитав данную статью, возможно, вы сможете подцепить тёлочку блеснуть умом в узких кругах. :)

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

logger::info() << "Тестовое сообщение номер " << 1;

Самая главная проблема была в том, чтобы воткнуть ‘\n’ в конце сообщения. Для этого в info() был примерно следующий код:

InfoStream info()
{
    static InfoStream stream;
    // std::endl пишет в ostream '\n' и делает flush()
    stream << std::endl;
    return stream;
}

Последняя строка завершалась только при закрытии файла (в деструкторе класса InfoStream). Первый flush игнорировался. Это все кажется простым, если мы пишем лог в обычный текстовый файл.

Когда лог стал в формате html, начались пляски с бубном.

Во-первых, в html надо записать заголовок, во-вторых, красиво оформить начало строки, в-третьих, красиво закончить строку, в-четвертых, вставить теги в конце файла. Это было реализовано, но совместимость между использованием текстового лога и html в некотором смысле пропала (базовый класс для обоих типов выглядел монстром).

Совсем недавно пришлось копаться во внутренностях. Задача стояла — вынести этот логгер в отдельный набор заголовков а-ля boost. Посмотрев на этот код, я плюнул и решил написать логгер заново. :)

Вот как это было реализовано:

class logger
{
public:
    class internal_stream
    {
    public:
        template <class T>
        internal_stream & operator <<(const T & value)
        {
            logger << value;
            return *this;
        }
 
        ~internal_stream() {}
 
    private:
        // Может использоваться только классом logger
        internal_stream(logger & stream) 
            : mStream(stream)
        {};
 
        logger & mStream;
    };
 
    internal_stream info()
    {
        return internal_stream(*this);
    }
};

Как это все работает:

  • в конструкторе logger — подготавливаем файл для записи, в текстовом ничего не делаем, в html — записываем заголовок
  • в деструкторе logger — закрываем файл, в html формате закрываем теги body, html и т.д.
  • в конструкторе internal_stream — начало строки, в html формате пишем tr, td и все что нравится
  • в деструкторе internal_stream — окончание строки, в текстовом это просто ‘\n’, в html закрываем теги tr и т.д.

Если сделать logger шаблонным классом, то можно реализовать разные буферы вывода. Начиная от текстового, html и xml и заканчивая сервером для приема и клиента для отправки сообщений по сети.

Блог Евгения Жирнова