Комментарии 60
Например, потому что в операционной системе эффективно работает кэш файловой системы,
mysql, например, чтобы избежать двойного кэширования использует O_DIRECT
почему-то в компиляторе MSVC в режиме debug очень "тормозят" итераторы
Непроверенная догадка: может компилятор добавляет в сгенерированный код дополнительные проверки, чтобы ловить всякие внештатные ситуации и облегчать отладку?
А если маппить на память, будет быстрее или нет?
И вы нигде лицензию не указали, возможно это стоит исправить.
Да, спасибо про лицензию. Да, однозначно mmap и/или флаг O_DIRECT в open могут помочь, но это все же вызовы OS, а не C++. Поэтому на текущем этапе избегал (это часть требований).
Ну как бы да, а с другой стороны вы уже используете _fseeki64 так что код все равно прибит к одной ОС :)
На самом деле мне не очевидно даст ли mmap какой-либо выигрыш кроме по умолчанию не блокирующей выгрузки на диск, которую в БД все равно приходится избегать.
ну, _fseeki64 можно будет заменить на обычный, это гораздо менее ОС-зависимая функция (базовые открыть, закрыть, считать, записать файл сидят в либс https://www.gnu.org/software/libc/manual/2.36/html_mono/libc.html#Portable-Positioning )
Вот тут чуваки в соавторстве с Andy Pablo говорят что MMAP для баз данных - плохая идея:
Are You Sure You Want to Use MMAP in Your Database Management System? https://www.cidrdb.org/cidr2022/papers/p13-crotty.pdf
Поэтому, только если IO_DIRECT
скорость I/O накопителя (HDD/SSD) в сотни раз медленнее, чем работа с оперативной памятью (RAM)
А вот и нет. Скорость линейного чтения из DDR4 порядка 20-25GB/s. При этом вполне себе доступны NVMe SSD со скоростью чтения 6-7GB/s.
"DDR4 порядка 20-25GB/s"
С одного канала, одной планки. Обычно каналов, планок, в количестве от 2 до 8 и больше, соответственно и скорость в 2-8 раза больше. И если сравнивать с каким медленным SSD/SATA, то разница вполне может на сотню потянуть ;)
одной планки
Ну так и SSD можно несколько штук поставить.
И завести в raid, иначе скорость не вырастет.
А 7 ГБ/c с SSD в типичных нагрузках вы и сейчас не увидите. Увидите 70-100Мб/c в однопоток. Ну а ваши базы данных в однопоток и пишут и писать в 50 потоков, чтобы нагрузить SSD параллельной записью, просто не способны.
SSD одним потоком последовательно вполне пишут свои 7ГБ\с. Вот со случайным доступом уже всё несколько грустнее.
Ссылку на бенчмарк можно c 7ГБ в один поток последовательной записи?
да пожалуйста
общедоступная информация, которая находится за минуту
4k 1Q 1T: 106 Мб чтение и 365 Мб запись. И это на файле в 1ГБ, то есть не пробивая SLC-кеш на запись, а как пробьете, скорость на запись упадет в разы.
Если же говорить про 1M Q8 T1, то это фактически пишут пачками по 8 Мб и в SSD действует внутренний распараллеливающий механизм, то есть это для ОС запись последовательная, а по факту она параллельная и на файле в 1ГБ это вообще тест ОЗУ SSD. А надо писать так, чтобы если уж такие объемы записи, чтобы пробить SLC кеш диска и тогда у вас скорость тоже драматическим образом упадет.
Поэтому дайте нормальный бенчмарк на файле с 200 Гб или такого размера, чтобы пробить SLC-кеш конкретного SSD (и уж тем более его ОЗУ). Лучше всего fio.
и на файле в 1ГБ это вообще тест ОЗУ SSD
AFAIK нет, RAM SSD используется в первую очередь для хранения таблицы транслятора, что же до кэширования записи, оно происходит только в рамках формирования целых страниц (NAND просто не умеет писать за раз меньше, чем страницу, которая сегодня значительно больше сектора, которым оперирует ОС).
Поэтому дайте нормальный бенчмарк на файле с 200 Гб или такого размера, чтобы пробить SLC-кеш конкретного SSD (и уж тем более его ОЗУ).
root@debian:~# fio -ioengine=libaio -sync=0 -direct=1 -name=test -bs=64k -iodepth=1 -rw=write -runtime=300 -filename=/dev/nvme3n1 -numjobs=1
test: (g=0): rw=write, bs=(R) 64.0KiB-64.0KiB, (W) 64.0KiB-64.0KiB, (T) 64.0KiB-64.0KiB, ioengine=libaio, iodepth=1
fio-3.25
Starting 1 process
Jobs: 1 (f=1): [W(1)][100.0%][w=2216MiB/s][w=35.5k IOPS][eta 00m:00s]
test: (groupid=0, jobs=1): err= 0: pid=665472: Wed Jan 4 02:45:51 2023
write: IOPS=35.4k, BW=2213MiB/s (2321MB/s)(648GiB/300001msec); 0 zone resets
slat (nsec): min=2359, max=53196, avg=2517.88, stdev=116.96
clat (nsec): min=430, max=2151.0k, avg=25371.04, stdev=5988.31
lat (usec): min=24, max=2153, avg=27.92, stdev= 5.99
clat percentiles (usec):
| 1.00th=[ 25], 5.00th=[ 25], 10.00th=[ 25], 20.00th=[ 25],
| 30.00th=[ 25], 40.00th=[ 25], 50.00th=[ 25], 60.00th=[ 26],
| 70.00th=[ 26], 80.00th=[ 27], 90.00th=[ 27], 95.00th=[ 28],
| 99.00th=[ 29], 99.50th=[ 29], 99.90th=[ 36], 99.95th=[ 36],
| 99.99th=[ 108]
bw ( MiB/s): min= 2179, max= 2227, per=100.00%, avg=2214.44, stdev= 9.85, samples=599
iops : min=34866, max=35644, avg=35430.98, stdev=157.65, samples=599
lat (nsec) : 500=0.01%
lat (usec) : 20=0.01%, 50=99.97%, 100=0.02%, 250=0.01%, 500=0.01%
lat (usec) : 750=0.01%, 1000=0.01%
lat (msec) : 2=0.01%, 4=0.01%
cpu : usr=6.14%, sys=9.53%, ctx=10624294, majf=0, minf=14
IO depths : 1=100.0%, 2=0.0%, 4=0.0%, 8=0.0%, 16=0.0%, 32=0.0%, >=64=0.0%
submit : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
complete : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
issued rwts: total=0,10624239,0,0 short=0,0,0,0 dropped=0,0,0,0
latency : target=0, window=0, percentile=100.00%, depth=1
Run status group 0 (all jobs):
WRITE: bw=2213MiB/s (2321MB/s), 2213MiB/s-2213MiB/s (2321MB/s-2321MB/s), io=648GiB (696GB), run=300001-300001msec
Disk stats (read/write):
nvme3n1: ios=46/10617848, merge=0/0, ticks=2/273002, in_queue=273004, util=100.00%
samsung pm9a3
на блоках 4К ожидаемо будет меньше (≈350), на блоках 1М — больше (≈3500).
и дело тут не только в распараллеливании записи со стороны накопителя, а и в накладных расходах на сисколы и т. п.
По тем данным, что я встречал, стоимость переключения контекста оценивается в 1 мксек. При 35к IOPS, если каждая операция IO приводит к переключению контекста, получаем 35k * 1 мксек = 0.035 сек - оверхед на переключение контекста в секунду на операции IO. Таким образом можно предположить, что сисколы имеют вклад в IOPS на уровне процентов и в разы влиять на IOPS не должны.
Нет, это не тест ОЗУ накопителя. Потому что есть гарантии по сохранности записанного в случае отключения электропитания. Если накопитель подтвердил завершение конкретной операции, это значит, что данные попали в энергонезависимую память.
Да, если писать много, то скорость падает, но у хороших накопителей это все еще порядка 1.5 Гб/сек
Да я бы не был так уверен в том что есть гарантии везде, например: https://mobile.twitter.com/xenadu02/status/1495693475584557056
ну там особый случай. разработчики macos решили, что fsync можно смело игнорировать
https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/fsync.2.html
Ну в данном случае соглашусь, что запись первые 13% - это не ОЗУ, а видимо SLC-кеш, так как вряд ли на SSD ОЗУ 128 ГБ под кеш записи выделено.
Но:
1) нет никаких гарантий, что AIDA64 делает fsync в процессе бенча и если делает, то после скольки блоков она его делает?
2) SSD с конденсаторами при fsync сразу врут, что записали на энергонезависимую память, так как могут гарантировать запись за счет конденсаторов и тут рядом есть бенч на эту тему: https://habr.com/ru/post/708768/comments/#comment_25073958
По факту если взять скорость записи в ячейку, то видимо она сильно не выросла за эти годы, а прирост линейной скорости записи происходит за счет возможности параллельной записи (ваши данные, которые вы пишите мегабайтами в один поток по факту разбиваются на несколько очередей и пишутся параллельно в разные микросхемы SSD (не знаю как их правильно называть). Чтобы запись внутри параллелилась, надо или чтобы была большая очередь на запись или блоки на запись сами были большими (сотни кбайт или мегабайты).
Но тут, конечно, что увидеть хочешь в бенче. Если хочешь примерно увидеть скорость записи в микросхему памяти, надо долбить записью мелким блоком (скажем в 4к) и после каждого блока fsync вызывать. Ну и диск при этом без кондера должен быть и не врать о записи сразу же по получению fsync.
Но этот бенч будет оценкой снизу, реальная нагрузка будет более оптимальна для распараллеливания.
Если хочешь примерно увидеть скорость записи в микросхему памяти, надо долбить записью мелким блоком (скажем в 4к) и после каждого блока fsync вызывать.
только это не будет временем записи 4кб в nand.
мы пишем 4кб, а накопителю нужно сбросить целую страницу (16кб или больше), обновить таблицу транслятора и сбросить её изменение тоже (это опять минимум страница).
WAF получается очень большой, как и нагрузка на сборщик мусора.
По факту если взять скорость записи в ячейку, то видимо она сильно не выросла за эти годы, а прирост линейной скорости записи происходит за счет возможности параллельной записи
если про latency ещё можно сказать, что накопитель врёт, то именно про скорость записи не понимаю ваших претензий. у ssd очень небольшой буфер на запись, и он сбрасывает данные оттуда в nand именно с той скоростью, что мы видим в тесте.
ну а что высокая скорость достигается в том числе и за счёт многоканальности контроллера ssd — не вижу в этом ничего плохого.
кстати, о задержках работы с nand: если запись кэшируется, то на чтении задержки честные. и в топовых накопителях они тоже постепенно улучшаются.
если во времена sata ssd типичным было время доступа 100+ мкс, то сейчас у приличных datacenter nvme время доступа 50-70 мкс, а некоторые десктопные накопители выдают в тестах и чуть меньше 50 мкс
Согласен. Но размер страницы памяти скорее всего не будет указан в даташите накопителя, да и у разных накопителей размер страницы может быть разным и при превышении размера можно столкнуться с распараллеливанием. Поэтому не так очевидно что измерять в качестве показателя, да и опять же к реальной нагрузке этот показатель будет спорное отношение иметь, больше теоретический интерес.
Опытным путем на своем SSD замерял, что лучшая скорость записи/чтения блоками 512Кб, а для HDD постраничный 8К (уже не 4Кб). Хотя все это зависит больше от самого железа.
Ну почему же. Данные могут писать в несколько потоков. А вот wal-журнал, да. В один поток.
Но все равно раз запись двойная, быстрее чем в один поток в wal не будет.
Да еще и частые fsync-и.
Да еще и частые fsync-и
не совсем в тему, но БД, всё-таки, это обычно datacenter ssd, на которых штраф за fsync околонулевой.
Сейчас нет данных под рукой, но по памяти штраф не нулевой даже у Optane. А есть бенч где нулевой?
root@debian:~# fio -ioengine=libaio -sync=1 -direct=1 -name=test -bs=4k -iodepth=1 -rw=randwrite -runtime=300 -filename=/dev/nvme3n1 -numjobs=1
test: (g=0): rw=randwrite, bs=(R) 4096B-4096B, (W) 4096B-4096B, (T) 4096B-4096B, ioengine=libaio, iodepth=1
fio-3.25
Starting 1 process
Jobs: 1 (f=1): [w(1)][100.0%][w=336MiB/s][w=86.1k IOPS][eta 00m:00s]
test: (groupid=0, jobs=1): err= 0: pid=665989: Wed Jan 4 03:02:25 2023
write: IOPS=85.7k, BW=335MiB/s (351MB/s)(98.0GiB/300001msec); 0 zone resets
slat (nsec): min=1101, max=49936, avg=1205.22, stdev=144.39
clat (nsec): min=230, max=114371, avg=9983.13, stdev=600.79
lat (nsec): min=10065, max=115572, avg=11216.26, stdev=639.44
clat percentiles (nsec):
| 1.00th=[ 9664], 5.00th=[ 9664], 10.00th=[ 9664], 20.00th=[ 9792],
| 30.00th=[ 9792], 40.00th=[ 9792], 50.00th=[ 9792], 60.00th=[ 9920],
| 70.00th=[10048], 80.00th=[10176], 90.00th=[10304], 95.00th=[10432],
| 99.00th=[11328], 99.50th=[12224], 99.90th=[18304], 99.95th=[18816],
| 99.99th=[20096]
bw ( KiB/s): min=305064, max=346504, per=100.00%, avg=342901.90, stdev=4658.80, samples=599
iops : min=76266, max=86626, avg=85725.42, stdev=1164.71, samples=599
lat (nsec) : 250=0.01%, 500=0.01%, 750=0.01%, 1000=0.01%
lat (usec) : 4=0.01%, 10=68.29%, 20=31.69%, 50=0.01%, 100=0.01%
lat (usec) : 250=0.01%
cpu : usr=10.61%, sys=16.50%, ctx=25697354, majf=0, minf=11
IO depths : 1=100.0%, 2=0.0%, 4=0.0%, 8=0.0%, 16=0.0%, 32=0.0%, >=64=0.0%
submit : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
complete : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
issued rwts: total=0,25697868,0,0 short=0,0,0,0 dropped=0,0,0,0
latency : target=0, window=0, percentile=100.00%, depth=1
Run status group 0 (all jobs):
WRITE: bw=335MiB/s (351MB/s), 335MiB/s-335MiB/s (351MB/s-351MB/s), io=98.0GiB (105GB), run=300001-300001msec
Disk stats (read/write):
nvme3n1: ios=46/25685523, merge=0/0, ticks=3/245296, in_queue=245299, util=100.00%
root@debian:~# fio -ioengine=libaio -sync=0 -direct=1 -name=test -bs=4k -iodepth=1 -rw=randwrite -runtime=300 -filename=/dev/nvme3n1 -numjobs=1test: (g=0): rw=randwrite, bs=(R) 4096B-4096B, (W) 4096B-4096B, (T) 4096B-4096B, ioengine=libaio, iodepth=1
fio-3.25
Starting 1 process
Jobs: 1 (f=1): [w(1)][100.0%][w=338MiB/s][w=86.6k IOPS][eta 00m:00s]
test: (groupid=0, jobs=1): err= 0: pid=666062: Wed Jan 4 03:07:26 2023
write: IOPS=86.3k, BW=337MiB/s (353MB/s)(98.8GiB/300001msec); 0 zone resets
slat (nsec): min=1090, max=107487, avg=1182.30, stdev=143.24
clat (nsec): min=230, max=66686, avg=9922.23, stdev=594.35
lat (nsec): min=9885, max=118733, avg=11131.44, stdev=634.03
clat percentiles (nsec):
| 1.00th=[ 9664], 5.00th=[ 9664], 10.00th=[ 9664], 20.00th=[ 9664],
| 30.00th=[ 9664], 40.00th=[ 9792], 50.00th=[ 9792], 60.00th=[ 9792],
| 70.00th=[ 9920], 80.00th=[10048], 90.00th=[10176], 95.00th=[10432],
| 99.00th=[11200], 99.50th=[12096], 99.90th=[18304], 99.95th=[18560],
| 99.99th=[20096]
bw ( KiB/s): min=267336, max=348400, per=100.00%, avg=345507.41, stdev=5421.34, samples=599
iops : min=66834, max=87100, avg=86376.73, stdev=1355.35, samples=599
lat (nsec) : 250=0.01%, 500=0.01%
lat (usec) : 2=0.01%, 4=0.01%, 10=75.96%, 20=24.02%, 50=0.01%
lat (usec) : 100=0.01%
cpu : usr=10.37%, sys=16.53%, ctx=25888992, majf=0, minf=12
IO depths : 1=100.0%, 2=0.0%, 4=0.0%, 8=0.0%, 16=0.0%, 32=0.0%, >=64=0.0%
submit : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
complete : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
issued rwts: total=0,25889393,0,0 short=0,0,0,0 dropped=0,0,0,0
latency : target=0, window=0, percentile=100.00%, depth=1
Run status group 0 (all jobs):
WRITE: bw=337MiB/s (353MB/s), 337MiB/s-337MiB/s (353MB/s-353MB/s), io=98.8GiB (106GB), run=300001-300001msec
Disk stats (read/write):
nvme3n1: ios=46/25875310, merge=0/0, ticks=3/245584, in_queue=245586, util=100.00%
всё тот же samsung pm9a3. на тех intel nvme, что тестировал, та же картина
А как вы думаете, как часто устанавливают дисковый raid при одной планке памяти. И ниже уже написали про latency, которая намного отличается.
только префетч не всегда можно устроить, а latency отличается на три порядка (50-100 нс против 50+ мкс)
С новым годом!
Всё хорошо, но то что код стайл будто какой-то неконсистентый по исходнику, мешает восприятию.
Шикарная работа. Очень шикарная.
А насчёт интерпретатора - не готов ответить на данный момент. Железо... Ограничения... Бюджеты...
Отличная работа.
А учитывая, что вы это делаете ради самообразования \ любви к искусству (программирования) - снимаю шляпу.
ПС
А транзакции планируются?
Мне всегда казалось, что DBMS это что-то про гарантии консистентности данных (ну то есть по-простому начиная с транзакций).
Спасибо! ACID - хотелось бы реализовать, но пока не разобрался как это делается в NoSQL.
А если вместо хэшмапы использовать битовые поля?
Для поиска бит можно использовать либо интринсики либо быстрые алгоритмы, например такой:
Индекс бита это номер страницы.
Индекс * размер страницы => смещение в mmap файле.
// возвращает размер _bits в 32 битных беззнаковых словах.
int size( bool units = true );
// получить индекс бита в 1
int getMSB() {
int r = -1;
#ifndef MACHINE_INSTRUCTIONS
const char lt[ 256 ] =
{
#define LT(n) n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n
- 1, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3,
LT( 4 ), LT( 5 ), LT( 5 ), LT( 6 ), LT( 6 ), LT( 6 ), LT( 6 ),
LT( 7 ), LT( 7 ), LT( 7 ), LT( 7 ), LT( 7 ), LT( 7 ), LT( 7 ), LT( 7 )
};
register quint32 t, tt, v;
for( int i = size( true ) - 1; i >= 0; i-- ) {
v = _bits[ i ];
if( v ) {
if( tt = v >> 16 ) {
r = ( t = tt >> 8 ) ? 24 + lt[ t ] : 16 + lt[ tt ];
}
else {
r = ( t = v >> 8 ) ? 8 + lt[ t ] : lt[ v ];
}
return ( i * 32 + r );
}
}
#else
register unsigned long index;
register quint32 v;
for( int i = size( true ) - 1; i >= 0; i-- ) {
v = _bits[ i ];
if( v ) {
if( _BitScanReverse( &index, v ) ) {
return i * ( sizeof( quint32 ) * 8U ) + index;
}
}
}
#endif
return r;
}
// количество установленных бит
int count() {
register quint32 v = 0, c = 0;
register int r = 0, i = 0, sizeInWords = size( true );
#ifndef MACHINE_INSTRUCTIONS
for( ; i < sizeInWords; i++ ) {
v = _bits[ i ];
v = v - ( ( v >> 1 ) & 0x55555555 );
v = ( v & 0x33333333 ) + ( ( v >> 2 ) & 0x33333333 );
c = ( ( v + ( v >> 4 ) & 0xF0F0F0F ) * 0x1010101 ) >> 24;
r += c;
}
#else
Q_UNUSED( v );
Q_UNUSED( c );
for( ; i < sizeInWords; i++ ) {
r += __popcnt( _bits[ i ] );
}
#endif
return r;
}
// set bit
// index >= 0 && < sizeInBits -- set bit by index
// otherwise -- set all bits
void set( int index = -1 ) {
if( index >= 0 ) {
if( index < size() )
_bits[ index / ( sizeof( quint32 ) * 8U ) ] |= ( 1U << ( index % ( sizeof( quint32 ) * 8U ) ) );
}
else {
_bits.fill( ~( 0U ) );
}
}
// clear bit
// index >= 0 && < sizeInBits -- clear bit by index
// otherwise -- clear all bits
void clear( int index = -1 ) {
if( index >= 0 ) {
if( index < size() ) {
quint32 bit = ( 1U << ( index % ( sizeof( quint32 ) * 8U ) ) );
int chunk = index / ( sizeof( quint32 ) * 8U );
_bits[ chunk ] |= bit;
_bits[ chunk ] ^= bit;
}
}
else {
_bits.fill( 0 );
}
}
//index >= 0 && < sizeInBits -- is bit set by index?
// otherwise -- is all bits set?
bool isSet( int index = -1 ) {
if( index >= 0 ) {
if( index < size() )
return _bits[ index / ( sizeof( quint32 ) * 8U ) ] & ( 1U << ( index % ( sizeof( quint32 ) * 8U ) ) );
}
else {
for( int i = 0, sizeInWords = size( true ); i < sizeInWords; i++ ) {
if( _bits[ i ] != ~( 0U ) ) {
return false;
}
}
return true;
}
return false;
}
//index >= 0 && < sizeInBits -- is bit clear by index?
// otherwise -- is all bits clear?
bool isClear( int index = -1 ) {
if( index >= 0 ) {
if( index < size() )
return !( _bits[ index / ( sizeof( quint32 ) * 8U ) ] & ( 1U << ( index % ( sizeof( quint32 ) * 8U ) ) ) );
}
else {
for( int i = 0, sizeInWords = size( true ); i < sizeInWords; i++ ) {
if( _bits[ i ] ) {
return false;
}
}
return true;
}
return false;
}
На всякий случай работающий код.
Возможно, в операции read если position кратен PAGE_SIZE и length равна PAGE_SIZE, то вы будете искать/читать 2 страницы вместо 1.
Возможно, вы очень оптимистичны, считая, что разница steady_timer time_point выдаёт наносекунды. Желательно использовать duration_cast (https://en.cppreference.com/w/cpp/chrono/duration/duration_cast);
Для хобби, возможно, это некритично:
[наверное что-то просмотрел и скопировать класс нельзя, но] лучше явно запретить копирование, а вот перенос можно бы и сделать.
гарантии на исключения вообще никакие. У вас bad_alloc может вылетать и ... ничего.
Спасибо за советы! Про read страниц проверю, а вот про время duration_cast не знал. По исключениям подумаю как обрабатывать.
Спасибо огромное за review! Действительно, страницы читались дважды при position кратном PAGE_SIZE и length равна PAGE_SIZE. Поправил. И касательно копирования - запретил. Обработку bad_alloc добавил.
в 2000 году был мне заказ сделать обработку данных на примере одной программы написанной очень крутым (для меня) программистом, который, по слухам, уехал в США и работал уже в Майкрософт.
Я знал только Delphi и LocalBDE и поэтому делал реализацию на них. У него проект был на C+WinAPI+какой-то бинарный формат БД.
Больше всего была разница в объемах файлов БД, у меня месяц работы выражался в 2-3 мегабайт (не гигабайт) данных, для транспортировки на одной дискете использовал ZIP, выгрузка сразу шла в архив при экспорте. А у гуру в прошлом проекте данные за год умещались в 200 килобайт и плохо паковались, подозреваю что он сам всё сжимал в какой-то LZ вариант. Тогда я понял, что мне не стать хорошим программистом XD (и не стал). А смотря на поделки современных разработчиков уверен что сделал бы отличную карьеру если бы пошел этим путём, ибо сегодня говнокодеры в почёте.
что-то я сомневаюсь, что самописная БД в 2000 году — признак хорошего программиста
Boson — разработка СУБД «с нуля» (часть I)