[an error occurred while processing this directive]

Сожмите ваши данные.

(дополненный и исправленный вариант)
 

Во многих случаях программистской практики приходится сталкиваться с проблемой хранения данных. Данные стремятся заполонить все свободное место на диске, их нужно периодически передвигать с одного диска на другой, пытаясь освободить место для следующей порции данных, засовывать в архивы, архивы где-то хранить, мучительно вспоминать, кто, когда и куда засунул то, что вам потребовалось именно сейчас. Заканчивается все это сообщением "Read error" при попытке восстановить данные с вашей дискеты  трехлетней давности.

Как решить проблему громоздких данных, как переложить с бестолкового пользователя на вашу программу задачу сжатия данных, как обеспечить сохранность ваших данных от умышленного или неумышленного изменения, как сжать ваши данные намного лучше стандартных компрессоров, как  ускорить работу вашей программы с этими медленными жесткими дисками,  как, наконец, сократить время загрузки ваших данных через эти медленные каналы передачи данных ? Нет ничего проще! Но обо всем по порядку.

Неявные методы сжатия данных.

Простейшим способом борьбы с объемом данных, совершенно не требующем от программиста никаких усилий, является использование утилит прозрачного сжатия данных "на лету" типа STACKER, DriveSpace или ZipStream, впрочем в связи с ростом емкости жестких дисков подобные утилиты все более выходят из моды.
 

Файловые компрессоры.

Можно не изобретать велосипед и использовать стандартные архиваторы и/или файловые копрессоры Arj, Rar, Zip, Tar+Gzip или менее стандартные HA и Bzip2. Записываем свои данные во временный файл, рождаем дочерний процесс с помощью функции из семейства spawn, в котором используем вызов одного из копрессоров для сжатия нашего временного файла. При чтении данных используем обратную процедуру. Собственно говоря именно так работают коммандеры класса FC и  Far с содержимым архивов при обеспечении возможности прозрачного редактирования архивных файлов. Выбор того или иного  архиватора и/или файлового копрессора  определяется разными обстоятельствами (распространенность, скорость работы, степень сжатия для конкрентых данных, лицензионные соображения)

Излишки обнуляйте.

Многие базы данных используют DBF формат, в котором каждая запись состоит из полей, каждое из которых является текстовой строкой фиксированной длины. Иногда некоторые поля не используются, но оставлены в силу исторических причин или для совместимости. Часто в таких неиспользованных полях оказывается записан различный мусор, поскольку не инициализированы поля структуры, которая программой отображается в DBF запись. Аналогично, если в DBF поля записываются строки Си (оканчивающиеся нулем), то в оставшемся месте поля записи может оказаться мусор.

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

Записывайте только значащие данные.

Более радикальным способом борьбы c DBF форматом может оказаться ...отказ от него. В самом деле, DBF часто используют как самый простой, известный и легкий способ хранения данных, и если не нужна совместимость в реальном времени с машинами баз данных, измените формат на более компактный и естественный. А для совместимости всегда можно сделать пункт меню "Save as..." или отдельностоящую утилиту. Итак, переходим к записям переменной длины, да еще от чисто текстовых - к двоичным. Текстовые поля меняем на байтовые длиной в sizeof(поле вашей структуры данных), для текстовых данных убираем постоянную длину строки.

Что же мы получим в итоге? Длина файла данных существенно сократилась, но в вашей программе никаких изменений не произошло, за исключением процедур записи и чтения файлов данных. В самом деле, модель данных (как массива структур вида struct MyRecord myrec[MAXRECNUM]) не изменилась.

Используем Zlib.

Году примерно в 1997   я прочитал в русскоязычной конференции fido7.su.os2.prog о библиотеке Zlib, general purpose data compression library. (Спасибо Dmitry V. Irtegov aka Fat Brother" и другим участникам)

Знакомство с Zlib превзошло все ожидания. Исходный код на pure C, вдобавок написанный в хорошем стиле. Полностью бесплатная библиотека, том числе и для коммерческого использования. Лицензионно чистая. Компилируется везде и чем угодно, есть интерфейсы для всякого безобразия наподобие C++Builder 3, Delphi 3, Java, Perl, Python, Visual Basic.  В настоящее время библиотека используется во многих проектах Open Source проектах (я знаю Mesa 3D , wxWindows,  MGL by SciTechsoft ).

Функции библиотеки позволяют использовать запись в файл/чтение из файла с прозрачным сжатием/распаковкой в стиле:

  void *buf;
  int len;
  gzFile zfp;
  ....
  zfp = gzopen("myfile","wb");
  ....
  gzwrite(zfp,buf,len);
/* gzprintf, gzputs, gzputc, gzflush, gzrewind, gztell и т.д. */
  ....
  gzclose(zfp);
  ....
  zfp = gzopen("myfile","wb");
  ....
  gzread(zfp,buf,len);
  ....
  gzclose(zfp);
Есть функции для сжатия данных в памяти из буфера в буфер:
  Byte *Compr, *UnCompr;
  int ComprLen,UncomprLen;
  ....
  Compr    = (Byte*)malloc(ComprLen);
  UnCompr  = (Byte*)malloc(UncomprLen);
/* заполняем uncompr */
  ....
  rc =   compress(Compr, &ComprLen, UnCompr, UncomprLen);
  ....
  rc = uncompress(UnCompr, &UncomprLen, Compr, ComprLen);
С такими возможностями ваша пpогpамма может использовать больше памяти, чем вам дает опеpационная система, и либо pаботать без свопиpования, либо иметь в pаспоpяжении больше 480Мб для OS/2 ver3-4 или 2Гб для остального.
Есть большое количество полезных функций, включая подсчет контрольной суммы.

Действительно, эта библиотека заслуживает только превосходных оценок, как и ее авторы - Jean-loup Gailly и Mark Adler.
Хотя следует заметить, что  визуально  сжатие данных происходит  несколько медленнее, чем при использовании zip'а.

Однако лучше один раз увидеть, чем сто раз услышать. Официальный ftp - ftp://ftp.cdrom.com/pub/infozip/zlib/. Этот сайт может оказаться перегруженным; мне, например, значительно больше повезло на ftp://uu.net/pub/.

Итак, тратим один день на освоение Zlib, после чего делаем zlib.lib или zlib.dll, переключаемся на следующую передачу и нажимаем на педаль газа. Данные, которые занимали 50Mb теперь требуют 10, а программа начала быстрее работать! В самом деле, самым медленным устройством для PII и далее сегодня становится жесткий диск. Дополнительные затраты процессорного времени на сжатие/распаковку данных с лихвой окупаются для быстрых процессоров сокращением времени записи на диск и чтения с диска.

Использование Zlib позволяет автоматически решить проблему защиты ваших данных. При необходимости всегда можно добавить более или менее сложный механизм шифрации данных, чтобы сильно продвинутый пользователь не смог вскрыть их при помощи gzip.

Достигая невозможного.

Можно ли еще больше повысить плотность упаковки наших данных? Оказывается, можно! На первый взгляд это кажется невероятным, тем не менее это так.

Посмотрим, как этого достичь. Откажемся на время от использования zlib и поэкспериментируем с нашими данными и различными "general purpose" файловыми компрессорами. Для всех современных упаковщиков (arj, zip, rar) результаты будут, как правило, примерно одинаковыми. Давайте однако используем голову программиста по своему прямому предназначению и вспомним, что внешний по отношению к ваше программе упаковщик понятия на имеет о структуре ваших данных и относится к ним как к битовому потоку повторяющихся данных со случайной составляющей. Другими словами, упаковщики настроены на однородные повторяющиеся входные данные, а мы постараемся без потери информации преобразовать наши данные к виду, наиболее похожему на однородные и повторяющиеся.

Транспонирование.

Например, в случае использования вышеописанной DBF-подобной структуры данных
  struct MyRecord
  { type A a;
    type B b;
    type C c;
    type D d;
  }
записи в файл данных будут помещены в порядке
  ABCD
  ABCD
  ....
  ABCD
Если мы сделаем транспонирование, т.е. запишем данные в порядке:
  AAAA....A
  BBBB....B
  СССС....C
  DDDD....D
то однородность наших данных резко повысится, соответственно возрастет и сжатие при использовании любых упаковщиков.

Разностная форма.

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

Рассмотрим пример. Пусть у нас есть байтовый массив длиной 256 элементов, в котором находятся числа:

  char m[256]={ 0,1,2,3,4,5,.....256};
запишем его в файл и посмотрим, как его сжимают упаковщики. Теперь запишем первый элемент и последовательные разности: m[0], m[1]-m[0],m[2]-m[1],....m[255]-m[254]. В файле у нас получится: 0,1,1,1,1,1,1,.....1.

Давайте почувствуем разницу:
Упаковщик Исходный массив, байт Разностная форма, байт
Rar 344 104
Zip 384 142

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

Приведем пример для большого файла. Исходный файл представляет собой словарь слов, отсортированных по алфавиту. Разделителем между словами служит, как обычно в языке Си,  0x00.  Используем несколько видоизмененную разностную форму: в каждом слове словаря начало слова, совпадающее с предыдущим, заменяем символом 0x00. Кроме того, попробуем использовать кодировку числа повторяющихся символов одним байтом со значениями  от 0 до 31, предварительно желательно удостоверится  в том, что в исходном файле  не встречаеются символы 1-31
 
Исходная форма "разностная" форма форма c  кодировкой повторений
адлер
адлера
адлерах
адлерам
адлерами
адлерберг
адлер
00000а
000000х
000000м
0000000и
00000берг
адлер
0x05а
0x06х
0x06м
0x07и
0x05берг
Файл исходный размер  bzip2 rar zip ha
исходный словарь 15139031 4017450 
26.6%
3506369
23.2%
 3406981
22.5%
3336947
22.0%
разностная форма 15139031 693849
4.6%
1045018
6.9%
1386780
9.1%
1295865
8.6%
форма c  кодировкой повторений 3454333 611229
4.04%
777152
5.13%
903968
5.97%
720009
4.76

Итого - разница в 2.5-5 раз !!! Обратите внимание на результаты для bzip2.

Формат данных.

Рассмотрим следующий пример. Запишем в нескольких форматах индексы 142651 слов  (1373024 - для  словаря из предыдущего примера) после сортировки. Значение индекса принимает значение от 0 до 142650 и может быть записано в 3 байта, в то время как значение sizeof(int) у нас - 4.

-------
int *st;
int n;
[...]
n = 142651; //1373024
[...]

   fp = fopen("s.bin","wb");
   for(i=0;i<n;i++)
      fwrite(&st[i],sizeof(int),1,fp);
   fclose(fp);

   fp = fopen("s.bin3","wb");
   for(i=0;i<n;i++)
      fwrite(&st[i],3,1,fp);
   fclose(fp);

   fp = fopen("s.hex","w");
   for(i=0;i<n;i++)
      fprintf(fp,"%x ",st[i]);
   fclose(fp);

   fp = fopen("s.dec","w");
   for(i=0;i<n;i++)
      fprintf(fp,"%i ",st[i]);
   fclose(fp);
 
Файл исходный размер  bzip2 rar zip ha
s.bin 570604
5492096
300590 
3563942
337057
3561196
344004 
2950497
333543
2837484
s.bin3 427953
4119072
303160
3609035
318496
3382974
354092
3642416
339152
3371983
s.hex 786002
8492688
345581 
3862500
400043 
3751773
388746
3632994
370852
3583934
s.dec 887447
9873082
349287
3944731
398698
3760490
389366 
3617739
373713 
3590173

Таким образом, использование более компактного исходного представления данных не всегда может приводить к меньшим размерам архивов, но с другой стороны, при этом уменьшается  выигрыш от использования файловых компрессоров.

Двумерные байтовые данные типа полутонового изображения.

Если изображение не слишком зашумлено и не слишком резкое (высокая корреляция соседних пикселов), можно использовать так называемое кодирование с предсказанием, которое в простейшем случае представляет собой комбинацию разностей по строкам и по элементам строки. Конечно, это будет не JPEG, но тем не менее... Тем более, что данными могут быть совсем не фотографии, а например, последовательности измерений с АЦП.
  int nx,ny,x,y,v;
  char mas[256][256], mas1[256][256];
  nx = 256;
  ny = 256;

  /* кодирование */
  mas1[0][0] = mas[0][0];
  for(x=1;x<nx;x++) mas1[0][x] = (mas[0][x]-mas[0][x-1]) & 0xff;
  for(y=1;y<ny;y++)
  { mas1[y][0] = (mas[y][0]-mas[y-1][0]) & 0xff;
    for(x=1;x<nx;x++)
    { v = mas[y][x-1]+mas[y-1][x];
      mas1[y][x] = (mas[y][x] - v) & 0xff;
    }
  }
  ....
  fwrite(mas1,ny,nx,fp);
  ...
  /* декодирование */
  fread(mas1,ny,nx,fp);
  mas[0][0] = mas1[0][0];
  for(x=1;x<nx;x++) mas[0][x] = (mas1[0][x]+mas1[0][x-1]) & 0xff;
  for(y=1;y<ny;y++)
  { mas[y][0] = (mas1[y][0]+mas[y-1][0]) & 0xff;
    for(x=1;x<nx;x++)
    { v = mas[y][x-1]+mas[y-1][x];
      mas1[y][x] = (mas1[y][x] + v) & 0xff;
    }
  }
Комбинация всех вышеописанных методов позволяет достичь для реальных данных коэффициентов сжатия, которые в 2-5 раз превышают коэффициенты сжатия при использовании стандартных файловых упаковщиков, при этом возрастает производительность и надежность работы вашей программы.

И пусть русские ракеты летают не только в космосе и хоккее!

(с) Евгений Коцюба, 1999 - 2001
[an error occurred while processing this directive]