Массив пользовательского размера

#python #c #arrays #cython

#python #c #массивы #cython

Вопрос:

Простая постановка задачи:

Возможно ли иметь массив данных пользовательского размера (3/5/6/7 байт) в C или Cython?

Предыстория:

Я столкнулся с серьезной нехваткой памяти при попытке закодировать сложный алгоритм. Алгоритм требует хранения умопомрачительного объема данных. Все данные расположены в непрерывном блоке памяти (наподобие массива). Данные — это просто очень длинный список [обычно] очень больших чисел. Тип чисел в этом списке / массиве является постоянным при заданном определенном наборе чисел (они работают почти как обычный массив C, где все числа одного типа в массиве)

Проблема:

Иногда неэффективно хранить каждое число в стандартном размере данных. Обычно обычными типами данных являются char, short, int, long и т.д… Однако, если я использую массив int для хранения типа данных, который находится только в диапазоне, который может быть сохранен в 3 байтах, на каждом числе я теряю 1 байт пространства. Это приводит к крайней неэффективности, и когда вы храните миллионы чисел, это приводит к разрыву памяти. К сожалению, другого способа реализовать решение алгоритма нет, и я считаю, что грубая реализация пользовательского размера данных — единственный способ сделать это.

Что я пробовал:

Я пытался использовать массивы символов для выполнения этой задачи, но преобразования между различными битами значений от 0 до 255 для формирования большего типа данных в большинстве случаев просто неэффективны. Часто существует математический метод, позволяющий брать символы и упаковывать их в большее число или брать это большее число и разделять его на отдельные символы. Вот крайне неэффективный алгоритм моей попытки сделать это, написанный на Cython:

 def to_bytes(long long number, int length):
    cdef:
        list chars = []
        long long m
        long long d
    
    for _ in range(length):
        m = number % 256
        d = number // 256
        chars.append(m)
        number = d
    
    cdef bytearray binary = bytearray(chars)
    binary = binary[::-1]
    return binary

def from_bytes(string):
    cdef long long d = int(str(string).encode('hex'), 16)
    return d
  

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

Комментарии:

1. Возможно, я что-то упускаю, но не использовать 1 байт для миллионов записей — это всего лишь порядка мегабайт. В современной архитектуре это довольно приемлемо, есть ли что-то еще, о чем вы говорите?

2. Ах да, существует не только один массив. Иногда объем оперативной памяти превышает стандартные 8 ГБ, особенно когда для хранения данных требуется всего 5 байт, но используется 8 байт (длиной). Я пытаюсь просто получить наиболее эффективную реализацию определенного размера данных в данном массиве, поэтому, когда таких массивов много, проблем не возникает.

3. Сложность заключается не просто в хранении байтов; это хранение размера каждого элемента. Похоже (и поправьте меня, если я ошибаюсь), что вы хотите сохранить каждое значение в наименьшем количестве октетов, необходимых для представления этого значения; не количество октетов, необходимых для представления всех значений этого типа, и при этом есть надежда повысить эффективность памяти ценой потенциальных штрафов за выравнивание. Если это так, то размер для каждого элемента необходим, и это не дешево (если вы никогда не кодировали ASN.1 DER / BER, вы не знаете, чего вам не хватает).

4. Некоторые языки используют ключевое слово packed или подобное для принудительного использования неэффективного по скорости, но экономичного по объему кода / данных. Осторожно отправляйтесь в этот темный лес, хотя, возможно, вы и не выйдете оттуда.

5. Не то чтобы это не интересный вопрос, но если у вас заканчивается оперативная память объемом всего 8 ГБ, кажется, что простым решением было бы купить другую флешку или арендовать на некоторое время лучшее оборудование. В наши дни любой вариант на удивление доступен.

Ответ №1:

Я думаю, важный вопрос заключается в том, нужно ли вам иметь доступ ко всем данным одновременно.

Если вам нужно получить доступ только к одному фрагменту данных одновременно

Если вам нужно получить доступ только к одному массиву за раз, то одной из возможностей Pythonic является использование массивов NumPy с типом данных uint8 и шириной по мере необходимости. Когда вам нужно работать с данными, вы расширяете сжатые данные на (здесь 3-октетные числа в uint32 ):

 import numpy as np

# in this example `compressed` is a Nx3 array of octets (`uint8`)
expanded = np.empty((compressed.shape[0], 4))
expanded[:,:3] = compressed
expanded[:, 3] = 0
expanded = expanded.view('uint32').reshape(-1)
  

Затем выполняются операции над expanded , который представляет собой одномерный вектор из N uint32 значений.

После того, как мы закончим, данные можно сохранить обратно:

 # recompress
compressed[:] = expanded.view('uint8').reshape(-1,4)[:,:3]
  

Время, затрачиваемое на каждое направление, составляет (на моей машине с Python) приблизительно 8 нс на элемент для примера выше. Использование Cython может не дать здесь большого преимущества в производительности, потому что почти все время тратится на копирование данных между буферами где-то в темных глубинах NumPy.

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


Конечно, тот же подход можно использовать и в C:

 #include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <sys/resource.h>

#define NUMITEMS 10000000

int main(void)
    {
    uint32_t *expanded;
    uint8_t * cmpressed, *exp_as_octets;
    struct rusage ru0, ru1;
    uint8_t *ep, *cp, *end;
    double time_delta;

    // create some compressed data
    cmpressed = (uint8_t *)malloc(NUMITEMS * 3);

    getrusage(RUSAGE_SELF, amp;ru0);

    // allocate the buffer and copy the data
    exp_as_octets = (uint8_t *)malloc(NUMITEMS * 4);
    end = exp_as_octets   NUMITEMS * 4;
    ep = exp_as_octets;
    cp = cmpressed;
    while (ep < end)
        {
        // copy three octets out of four
        *ep   = *cp  ;
        *ep   = *cp  ;
        *ep   = *cp  ;
        *ep   = 0;
        }
    expanded = (uint32_t *)exp_as_octets;

    getrusage(RUSAGE_SELF, amp;ru1);
    printf("Uncompressn");
    time_delta = ru1.ru_utime.tv_sec   ru1.ru_utime.tv_usec * 1e-6 
               - ru0.ru_utime.tv_sec - ru0.ru_utime.tv_usec * 1e-6;
    printf("User: %.6lf seconds, %.2lf nanoseconds per element", time_delta, 1e9 * time_delta / NUMITEMS);
    time_delta = ru1.ru_stime.tv_sec   ru1.ru_stime.tv_usec * 1e-6 
               - ru0.ru_stime.tv_sec - ru0.ru_stime.tv_usec * 1e-6;
    printf("System: %.6lf seconds, %.2lf nanoseconds per element", time_delta, 1e9 * time_delta / NUMITEMS);

    getrusage(RUSAGE_SELF, amp;ru0);
    // compress back
    ep = exp_as_octets;
    cp = cmpressed;
    while (ep < end)
       {
       *cp   = *ep  ;
       *cp   = *ep  ;
       *cp   = *ep  ;
       ep  ;
       }
    getrusage(RUSAGE_SELF, amp;ru1);
    printf("Compressn");
    time_delta = ru1.ru_utime.tv_sec   ru1.ru_utime.tv_usec * 1e-6 
               - ru0.ru_utime.tv_sec - ru0.ru_utime.tv_usec * 1e-6;
    printf("User: %.6lf seconds, %.2lf nanoseconds per element", time_delta, 1e9 * time_delta / NUMITEMS);
    time_delta = ru1.ru_stime.tv_sec   ru1.ru_stime.tv_usec * 1e-6 
               - ru0.ru_stime.tv_sec - ru0.ru_stime.tv_usec * 1e-6;
    printf("System: %.6lf seconds, %.2lf nanoseconds per element", time_delta, 1e9 * time_delta / NUMITEMS);
    }
  

Это сообщает:

 Uncompress
 User: 0.022650 seconds, 2.27 nanoseconds per element
 System: 0.016171 seconds, 1.62 nanoseconds per element
Compress
 User: 0.011698 seconds, 1.17 nanoseconds per element
 System: 0.000018 seconds, 0.00 nanoseconds per element
  

Код был скомпилирован с gcc -Ofast и, вероятно, относительно близок к оптимальной скорости. Системное время тратится на malloc . На мой взгляд, это выглядит довольно быстро, поскольку мы выполняем операции чтения в памяти со скоростью 2-3 ГБ / с. (Это также означает, что, хотя сделать код многопоточным было бы легко, преимущества в скорости могут быть незначительными.)

Если вы хотите добиться наилучшей производительности, вам нужно будет закодировать процедуры сжатия / распаковки для каждой ширины данных отдельно. (Я не обещаю, что приведенный выше код C является абсолютно самым быстрым на любой машине, я не просматривал машинный код.)

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

Если вместо этого вам нужно получить доступ только к одному значению здесь, а к другому там, Python не предложит никаких разумно быстрых методов, поскольку накладные расходы на поиск в массиве огромны.

В этом случае я предлагаю вам создать подпрограммы C для извлечения и возврата данных обратно. Смотрите technosaurus ответ. Существует множество хитростей, но проблем с выравниванием избежать невозможно.

Одним из полезных приемов при чтении массива нечетного размера может быть (здесь чтение 3 октетов из массива октетов compressed в uint32_t value ):

 value = (uint32_t *)amp;compressed[3 * n] amp; 0x00ffffff;
  

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

Ответ №2:

В C вы можете определить пользовательский тип данных для обработки сложностей с произвольным размером байта:

 typedef struct 3byte { char x[3]; } 3byte;
  

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

Комментарии:

1. @WhozCraig: Нет, они не будут. У него нет никаких требований к выравниванию; заполнение бесполезно. Демо-версия Ideone размером 3: ideone.com/FNcWxf (Однако нам придется изменить имя, поскольку ему не разрешается начинаться с цифры.)

2. @user2357112 вы абсолютно правы. Мне серьезно нужно прекратить публиковать комментарии без соответствующего добавления кофеина. Один элемент с 3 символами не имеет спецификаций выравнивания, даже упорядоченных в массиве из них. хороший улов. удаление ошибочного комментария; спасибо, что оставляете меня честным.

Ответ №3:

Вы можете использовать упакованное битовое поле. В GCC это выглядело бы как

 typedef struct __attribute__((__packed__)) {
    int x : 24;
} int24;
  

Для int24 x , x.x работает почти так же, как 24-разрядный int. Вы можете создать массив из них, и в нем не будет никаких ненужных дополнений. Обратите внимание, что это будет медленнее, чем использование обычных целых чисел; данные не будут выровнены, и я не думаю, что есть какая-либо инструкция для 24-разрядного чтения. Компилятору потребуется генерировать дополнительный код для каждого чтения и сохранения.

Комментарии:

1. Единственная проблема с этим методом заключается в том, что для «неправильного» порядкового номера потребуется поменять местами несколько байтов …. больше операций, чем если бы это были просто массивы символов.

2. @technosaurus: Я не уверен, что вы имеете в виду под этим. Почему нас волнует порядковый номер? Похоже, что нет никаких требований к расположению памяти; сам факт, что мы выбираем количество байтов, используемых для представления записи, означает, что мы не имеем дело с фиксированным расположением памяти, навязанным каким-либо внешним требованием.

3. Предположим, что эти данные были сгенерированы на x86 и этот код выполняется на x86 — все будет в порядке… пока он не будет скомпилирован / запущен на машине с другим порядковым номером (arm, mips, …), цифры будут неправильными… пример. 0xFFAA00 может быть интерпретирован как 0x00AAFF … или то, что диктует порядковый номер этой машины… для краткости / длинности существуют функции для перехода к порядку байтов конечного узла / из него в сетевой: htonl, htons, ntohl, ntohs … но в OP не упоминается порядок байтов, так что, вероятно, это в собственном порядке байтов их машины, иначе они бы заметили и упомянули об этом.

4. @technosaurus: Я почти уверен, что мы сами определяем порядок байтов.

Ответ №4:

MrAlias и user оба имеют хорошие моменты, так почему бы не объединить их?

 typedef union __attribute__((__packed__)) {
  int x : 24;
  char s[3];
} u3b;

typedef union __attribute__((__packed__)) {
  long long x : 56;
  char s[7];
} u7b;
  

Для больших объемов данных вы можете таким образом сэкономить немного памяти, но код почти наверняка будет медленнее из-за несогласованных обращений, которые он повлечет за собой. Для максимальной эффективности вы должны расширить их, чтобы выровнять по стандартной целочисленной длине и работать с ними (считывать массивы, кратные 4 или 8).

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

 int x = myu3b.s[0]|(myu3b.s[1]<<8)|(myu3b.s[2]<<16);
//or
int x = myu3b.s[2]|(myu3b.s[1]<<8)|(myu3b.s[0]<<16);
  

Этот метод может быть таким же быстрым после оптимизации (зависит от компилятора), если это так, вы можете просто использовать массивы символов и вообще пропустить объединение.

Ответ №5:

Я полностью поддерживаю подход с набором битов, просто следите за проблемами выравнивания. Если вы часто используете произвольный доступ, возможно, вам захочется убедиться, что вы соответствуете своей архитектуре cache cpu.

Кроме того, я бы предложил рассмотреть другой подход:

Вы могли бы распаковывать нужные вам данные «на лету», используя, например, zlib. Если вы ожидаете, что в потоке будет много повторяющихся значений, это может значительно сократить трафик ввода-вывода, а также объем памяти. (Предполагая, что потребность в произвольном доступе не слишком велика.) Смотрите здесь краткое руководство по zlib.

Ответ №6:

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

Проблема с packed битовыми полями заключается в том, что они не являются стандартными и не работают для чтения / записи на машинах с разным завершением. Мне пришло в голову, что little-endian — это просто решение этой проблемы … поэтому, делая вид, что хочет решить проблему с конечным порядком, трюк, казалось, заключался в том, чтобы хранить материал в формате little-endian. Для, скажем, 5-байтовых целых чисел: сохранить значение с начальным порядком просто, вы просто копируете первые 5 байт; загрузка не так проста, потому что вам нужно подписать extend .

Приведенный ниже код будет создавать массивы из целых чисел размером 2, 3, 4 и 5 байт со знаком: (а) принудительно использовать младший порядковый номер и (б) использовать packed битовые поля для сравнения (см. BIT_FIELD ). Как указано, он компилируется под gcc в Linux (64-разрядная версия).

В коде сделаны два летающих предположения:

  1. -пять чисел являются дополнением 2 или 1 (без знака и величины)!

  2. эти структуры с выравниванием == 1 могут быть прочитаны / записаны по любому адресу для структуры любого размера.

main Выполняет некоторое тестирование и синхронизацию. Он выполняет тот же тест на больших массивах: (а) «гибких» массивах с целочисленными длинами 2, 3, 4 и 5; и (б) простых массивах с целочисленными длинами 2, 4, 4 и 8. На моей машине здесь я получил (скомпилированный -O3, максимальная оптимизация):

 Arrays of 800 million entries -- not using bit-field
With 'flex' arrays of 10.4G bytes: took 20.160 secs: user 16.600 system 3.500
With simple arrays of 13.4G bytes: took 32.580 secs: user 14.680 system 4.910

Arrays of 800 million entries -- using bit-field
With 'flex' arrays of 10.4G bytes: took 22.280 secs: user 18.820 system 3.380
With simple arrays of 13.4G bytes: took 20.450 secs: user 14.450 system 4.620
  

Итак, используя достаточно общий код, целые числа специальной длины занимают больше времени, но, возможно, не так плохо, как можно было ожидать !! Версия с битовым полем выходит медленнее… У меня не было времени разобраться, почему.

Итак … для меня это выглядит выполнимо.

 /*==============================================================================
 * 2/3/4/5/... byte "integers" and arrays thereof.
 */
#include <stdint.h>
#include <stdbool.h>
#include <stdlib.h>
#include <stddef.h>
#include <unistd.h>
#include <memory.h>
#include <stdio.h>
#include <sys/times.h>
#include <assert.h>

/*==============================================================================
 * General options
 */
#define BIT_FIELD 0             /* use bit-fields (or not)  */

#include <endian.h>
#include <byteswap.h>

#if __BYTE_ORDER == __LITTLE_ENDIAN
# define htole16(x) (x)
# define le16toh(x) (x)

# define htole32(x) (x)
# define le32toh(x) (x)

# define htole64(x) (x)
# define le64toh(x) (x)

#else
# define htole16(x) __bswap_16 (x)
# define le16toh(x) __bswap_16 (x)

# define htole32(x) __bswap_32 (x)
# define le32toh(x) __bswap_32 (x)

# define htole64(x) __bswap_64 (x)
# define le64toh(x) __bswap_64 (x)
#endif

typedef int64_t imax_t ;

/*------------------------------------------------------------------------------
 * 2 byte integer
 */
#if BIT_FIELD
typedef struct __attribute__((packed)) { int16_t  i : 2 * 8 ; } iflex_2b_t ;
#else
typedef struct { int8_t b[2] ; } iflex_2b_t ;
#endif

inline static int16_t
iflex_get_2b(iflex_2b_t item)
{
#if BIT_FIELD
  return item.i ;
#else
  union
  {
    int16_t     i ;
    iflex_2b_t  f ;
  } x ;

  x.f = item ;
  return le16toh(x.i) ;
#endif
} ;

inline static iflex_2b_t
iflex_put_2b(int16_t val)
{
#if BIT_FIELD
  iflex_2b_t x ;
  x.i = val ;
  return x ;
#else
  union
  {
    int16_t     i ;
    iflex_2b_t  f ;
  } x ;

  x.i = htole16(val) ;
  return x.f ;
#endif
} ;

/*------------------------------------------------------------------------------
 * 3 byte integer
 */
#if BIT_FIELD
typedef struct __attribute__((packed)) { int32_t  i : 3 * 8 ; } iflex_3b_t ;
#else
typedef struct { int8_t b[3] ; } iflex_3b_t ;
#endif

inline static int32_t
iflex_get_3b(iflex_3b_t item)
{
#if BIT_FIELD
  return item.i ;
#else
  union
  {
    int32_t     i ;
    int16_t     s[2] ;
    iflex_2b_t  t[2] ;
  } x ;

  x.t[0] = *((iflex_2b_t*)amp;item) ;
  x.s[1] = htole16(item.b[2]) ;

  return le32toh(x.i) ;
#endif
} ;

inline static iflex_3b_t
iflex_put_3b(int32_t val)
{
#if BIT_FIELD
  iflex_3b_t x ;
  x.i = val ;
  return x ;
#else
  union
  {
    int32_t     i ;
    iflex_3b_t  f ;
  } x ;

  x.i = htole32(val) ;
  return x.f ;
#endif
} ;

/*------------------------------------------------------------------------------
 * 4 byte integer
 */
#if BIT_FIELD
typedef struct __attribute__((packed)) { int32_t  i : 4 * 8 ; } iflex_4b_t ;
#else
typedef struct { int8_t b[4] ; } iflex_4b_t ;
#endif

inline static int32_t
iflex_get_4b(iflex_4b_t item)
{
#if BIT_FIELD
  return item.i ;
#else
  union
  {
    int32_t     i ;
    iflex_4b_t  f ;
  } x ;

  x.f = item ;
  return le32toh(x.i) ;
#endif
} ;

inline static iflex_4b_t
iflex_put_4b(int32_t val)
{
#if BIT_FIELD
  iflex_4b_t x ;
  x.i = val ;
  return x ;
#else
  union
  {
    int32_t     i ;
    iflex_4b_t  f ;
  } x ;

  x.i = htole32((int32_t)val) ;
  return x.f ;
#endif
} ;

/*------------------------------------------------------------------------------
 * 5 byte integer
 */
#if BIT_FIELD
typedef struct __attribute__((packed)) { int64_t  i : 5 * 8 ; } iflex_5b_t ;
#else
typedef struct { int8_t b[5] ; } iflex_5b_t ;
#endif

inline static int64_t
iflex_get_5b(iflex_5b_t item)
{
#if BIT_FIELD
  return item.i ;
#else
  union
  {
    int64_t     i ;
    int32_t     s[2] ;
    iflex_4b_t  t[2] ;
  } x ;

  x.t[0] = *((iflex_4b_t*)amp;item) ;
  x.s[1] = htole32(item.b[4]) ;

  return le64toh(x.i) ;
#endif
} ;

inline static iflex_5b_t
iflex_put_5b(int64_t val)
{
#if BIT_FIELD
  iflex_5b_t x ;
  x.i = val ;
  return x ;
#else
  union
  {
    int64_t     i ;
    iflex_5b_t  f ;
  } x ;

  x.i = htole64(val) ;
  return x.f ;
#endif
} ;

/*------------------------------------------------------------------------------
 *
 */
#define alignof(t) __alignof__(t)

/*==============================================================================
 * To begin at the beginning...
 */
int
main(int argc, char* argv[])
{
  int count = 800 ;

  assert(sizeof(iflex_2b_t)  == 2) ;
  assert(alignof(iflex_2b_t) == 1) ;
  assert(sizeof(iflex_3b_t)  == 3) ;
  assert(alignof(iflex_3b_t) == 1) ;
  assert(sizeof(iflex_4b_t)  == 4) ;
  assert(alignof(iflex_4b_t) == 1) ;
  assert(sizeof(iflex_5b_t)  == 5) ;
  assert(alignof(iflex_5b_t) == 1) ;

  clock_t at_start_clock, at_end_clock ;
  struct tms at_start_tms, at_end_tms ;
  clock_t ticks ;

  printf("Arrays of %d million entries -- %susing bit-fieldn", count,
                                                      BIT_FIELD ? "" : "not ") ;
  count *= 1000000 ;

  iflex_2b_t* arr2 = malloc(count * sizeof(iflex_2b_t)) ;
  iflex_3b_t* arr3 = malloc(count * sizeof(iflex_3b_t)) ;
  iflex_4b_t* arr4 = malloc(count * sizeof(iflex_4b_t)) ;
  iflex_5b_t* arr5 = malloc(count * sizeof(iflex_5b_t)) ;

  size_t bytes = ((size_t)count * (2   3   4   5)) ;

  srand(314159) ;

  at_start_clock = times(amp;at_start_tms) ;

  for (int i = 0 ; i < count ; i  )
    {
      imax_t v5, v4, v3, v2, r ;

      v2 = (int16_t)(rand() % 0x10000) ;
      arr2[i] = iflex_put_2b(v2) ;

      v3 = (v2 * 0x100) | ((i amp; 0xFF) ^ 0x33) ;
      arr3[i] = iflex_put_3b(v3) ;

      v4 = (v3 * 0x100) | ((i amp; 0xFF) ^ 0x44) ;
      arr4[i] = iflex_put_4b(v4) ;

      v5 = (v4 * 0x100) | ((i amp; 0xFF) ^ 0x55) ;
      arr5[i] = iflex_put_5b(v5) ;

      r = iflex_get_2b(arr2[i]) ;
      assert(r == v2) ;

      r = iflex_get_3b(arr3[i]) ;
      assert(r == v3) ;

      r = iflex_get_4b(arr4[i]) ;
      assert(r == v4) ;

      r = iflex_get_5b(arr5[i]) ;
      assert(r == v5) ;
    } ;

  for (int i = count - 1 ; i >= 0 ; i--)
    {
      imax_t v5, v4, v3, v2, r, b ;

      v5 = iflex_get_5b(arr5[i]) ;
      b  = (i amp; 0xFF) ^ 0x55 ;
      assert((v5 amp; 0xFF) == b) ;
      r  = (v5 ^ b) / 0x100 ;

      v4 = iflex_get_4b(arr4[i]) ;
      assert(v4 == r) ;
      b  = (i amp; 0xFF) ^ 0x44 ;
      assert((v4 amp; 0xFF) == b) ;
      r  = (v4 ^ b) / 0x100 ;

      v3 = iflex_get_3b(arr3[i]) ;
      assert(v3 == r) ;
      b  = (i amp; 0xFF) ^ 0x33 ;
      assert((v3 amp; 0xFF) == b) ;
      r  = (v3 ^ b) / 0x100 ;

      v2 = iflex_get_2b(arr2[i]) ;
      assert(v2 == r) ;
    } ;

  at_end_clock  = times(amp;at_end_tms) ;

  ticks = sysconf(_SC_CLK_TCK) ;

  printf("With 'flex' arrays of %4.1fG bytes: "
                                  "took %5.3f secs: user %5.3f system %5.3fn",
      (double)bytes / (double)(1024 *1024 *1024),
      (double)(at_end_clock - at_start_clock)                 / (double)ticks,
      (double)(at_end_tms.tms_utime - at_start_tms.tms_utime) / (double)ticks,
      (double)(at_end_tms.tms_stime - at_start_tms.tms_stime) / (double)ticks) ;

  free(arr2) ;
  free(arr3) ;
  free(arr4) ;
  free(arr5) ;

  int16_t* brr2 = malloc(count * sizeof(int16_t)) ;
  int32_t* brr3 = malloc(count * sizeof(int32_t)) ;
  int32_t* brr4 = malloc(count * sizeof(int32_t)) ;
  int64_t* brr5 = malloc(count * sizeof(int64_t)) ;

  bytes = ((size_t)count * (2   4   4   8)) ;

  srand(314159) ;

  at_start_clock = times(amp;at_start_tms) ;

  for (int i = 0 ; i < count ; i  )
    {
      imax_t v5, v4, v3, v2, r ;

      v2 = (int16_t)(rand() % 0x10000) ;
      brr2[i] = v2 ;

      v3 = (v2 * 0x100) | ((i amp; 0xFF) ^ 0x33) ;
      brr3[i] = v3 ;

      v4 = (v3 * 0x100) | ((i amp; 0xFF) ^ 0x44) ;
      brr4[i] = v4 ;

      v5 = (v4 * 0x100) | ((i amp; 0xFF) ^ 0x55) ;
      brr5[i] = v5 ;

      r = brr2[i] ;
      assert(r == v2) ;

      r = brr3[i] ;
      assert(r == v3) ;

      r = brr4[i] ;
      assert(r == v4) ;

      r = brr5[i] ;
      assert(r == v5) ;
    } ;

  for (int i = count - 1 ; i >= 0 ; i--)
    {
      imax_t v5, v4, v3, v2, r, b ;

      v5 = brr5[i] ;
      b  = (i amp; 0xFF) ^ 0x55 ;
      assert((v5 amp; 0xFF) == b) ;
      r  = (v5 ^ b) / 0x100 ;

      v4 = brr4[i] ;
      assert(v4 == r) ;
      b  = (i amp; 0xFF) ^ 0x44 ;
      assert((v4 amp; 0xFF) == b) ;
      r  = (v4 ^ b) / 0x100 ;

      v3 = brr3[i] ;
      assert(v3 == r) ;
      b  = (i amp; 0xFF) ^ 0x33 ;
      assert((v3 amp; 0xFF) == b) ;
      r  = (v3 ^ b) / 0x100 ;

      v2 = brr2[i] ;
      assert(v2 == r) ;
    } ;

  at_end_clock  = times(amp;at_end_tms) ;

  printf("With simple arrays of %4.1fG bytes: "
                                  "took %5.3f secs: user %5.3f system %5.3fn",
      (double)bytes / (double)(1024 *1024 *1024),
      (double)(at_end_clock - at_start_clock)                 / (double)ticks,
      (double)(at_end_tms.tms_utime - at_start_tms.tms_utime) / (double)ticks,
      (double)(at_end_tms.tms_stime - at_start_tms.tms_stime) / (double)ticks) ;

  free(brr2) ;
  free(brr3) ;
  free(brr4) ;
  free(brr5) ;

  return 0 ;
} ;