доступ к длинному двухбитному представлению

TLDR; Вызывает ли следующий код неопределенное (или неопределенное) поведение?

#include <stdio.h>
#include <string.h>

void printme(void *c, size_t n)
{
  /* print n bytes in binary */
}

int main() {
  long double value1 = 0;
  long double value2 = 0;

  memset( (void*) &value1, 0x00, sizeof(long double));
  memset( (void*) &value2, 0x00, sizeof(long double));

  /* printf("value1: "); */
  /* printme(&value1, sizeof(long double)); */
  /* printf("value2: "); */
  /* printme(&value2, sizeof(long double)); */

  value1 = 0.0;
  value2 = 1.0;

  printf("value1: %Lf\n", value1);
  printme(&value1, sizeof(long double));
  printf("value2: %Lf\n", value2);
  printme(&value2, sizeof(long double));

  return 0;
}

На моей машине x86-64 результат зависит от конкретных флагов оптимизации, переданных компилятору (gcc-4.8.0, -O0 и -O1).

С -O0 я получаю

value1: 0.000000
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 
value2: 1.000000
00000000 00000000 00000000 00000000 00000000 00000000 00111111 11111111
10000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000

В то время как с -O1 я получаю

value1: 0.000000
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 
value2: 1.000000
00000000 00000000 00000000 00000000 00000000 01000000 00111111 11111111
10000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 

Обратите внимание на лишнюю 1 в предпоследней строке. Кроме того, раскомментирование инструкций печати после memset приводит к исчезновению 1. Кажется, это основано на двух фактах:

  1. long double дополняется, т. е. sizeof(long double) = 16, но используется только 10 байт.
  2. вызов memset может быть оптимизирован
  3. биты заполнения длинных двойников могут измениться без уведомления, т. е. операции с плавающей запятой над значением1 и значением2, по-видимому, скремблируют биты заполнения.

Я компилирую с -std=c99 -Wall -Wextra -Wpedantic и не получаю предупреждений, поэтому я не уверен, что это случай строгого нарушения алиасинга (но вполне может быть). Прохождение -fno-strict-aliasing ничего не меняет.

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

So:

  1. Это неопределенное поведение?
  2. Является ли это строгим нарушением алиасинга?

Спасибо.

редактировать: это код для printme. Я признаю, что просто вырезал и вставил откуда-то, не обращая на это особого внимания. Если вина здесь, я буду ходить вокруг стола со спущенными штанами.

void printme(void *c, size_t n)
{
  unsigned char *t = c;
  if (c == NULL)
    return;
  while (n > 0) {
    int q;
    --n;
    for(q = 0x80; q; q >>= 1) 
      printf("%x", !!(t[n] & q));
    printf(" ");
  }
  printf("\n");
}

person andreabedini    schedule 07.09.2013    source источник
comment
Я запускаю gcc 4.7.2 и вижу одинаковый вывод для -O0 и -O1.   -  person lurker    schedule 07.09.2013
comment
@mbrach да, я знаю, что это тоже зависит от версии компилятора. Спасибо.   -  person andreabedini    schedule 07.09.2013
comment
Мне нравится расширенная точность long double, и я благодарен, что GCC делает ее доступной, но я действительно ненавижу выбор GCC для представления ее 16 байтами.   -  person Pascal Cuoq    schedule 07.09.2013
comment
@PascalCuoq, на который можно повлиять с помощью флага -m96bit-long-double. См. gcc.gnu.org/onlinedocs/gcc/i386-and -x86_002d64-Options.html   -  person andreabedini    schedule 07.09.2013
comment
@PascalCuoq: это не выбор GCC, а выбор x86_64 psABI. Да, это немного раздражает, когда так много отступов, но я не вижу другого жизнеспособного варианта. Неспособность 32-разрядного x86 ABI выровнять long double привела к серьезному снижению производительности современных чипов.   -  person R.. GitHub STOP HELPING ICE    schedule 07.09.2013


Ответы (3)


Это неопределенное поведение?

Да. Биты заполнения не определены (*). Доступ к неопределенной памяти также может быть неопределенным поведением (это было неопределенное поведение в C90, и некоторые компиляторы C99 рассматривают его как неопределенное поведение. Также в обосновании C99 говорится, что доступ к неопределенной памяти предназначен для неопределенного поведения. Но сам стандарт C99 не говорит об этом так ясно, он только намекает для ловушек-представлений и может создаться впечатление, что если кто-то знает, что у него нет ловушек-представлений, он может получить неуказанные значения из неопределенной памяти). Заполняющая часть long double как минимум не указана.

(*) В сноске 271 C99 говорится: «Содержимое« отверстий », используемых в качестве заполнения для целей выравнивания внутри объектов структуры, не определено». Текст ранее относится к неуказанным байтам, но это только потому, что байты не имеют представлений прерываний.

Является ли это строгим нарушением алиасинга?

Я не вижу в вашем коде строгого нарушения псевдонимов.

person Pascal Cuoq    schedule 07.09.2013
comment
Спасибо. Имеет ли значение, если внутри printme void *c преобразуется в unsigned char * для печати? Будет ли это строгим нарушением алиасинга? - person andreabedini; 07.09.2013
comment
@beb0s Приведение к unsigned char * — один из правильных способов доступа к представлению без нарушения правил псевдонимов. - person Pascal Cuoq; 07.09.2013
comment
Я вообще не вижу здесь ничего определенного или неопределенного. sizeof(long double) возвращает 16 на своей машине, и, похоже, он обращается к 16 байтам. Конечно, он не показывает нам код, который это делает, так что мы не можем сказать наверняка. - person Lee Daniel Crocker; 07.09.2013
comment
@LeeDanielCrocker Когда sizeof(t) вычисляется как N для вашей платформы компиляции, это не означает, что вам разрешено читать все CHAR_BIT * N биты из объекта типа t. Это неверно для структур, которые могут иметь отступы, и неверно для базовых типов. Только unsigned char гарантированно не содержит битов заполнения. - person Pascal Cuoq; 07.09.2013
comment
Я уверен, что CHAR_BIT на его машине (Intel) равен 8, а GCC на Intel64 использует 128-битные удвоения. Заполнение не является его проблемой: биты, которые он показывает, не похожи ни на какое представление с плавающей запятой IEEE, будь то 10, 12 или 16 байтов. Ясно, что его printme() сломан, а его мысли о дополнении - просто отвлекающий маневр. - person Lee Daniel Crocker; 07.09.2013
comment
@LeeDanielCrocker Вы знаете, что почти все компиляторы для IA-32 и x86-64, которые предлагают тип long double, отличный от double, на самом деле предлагают 80-битный расширенный тип double из 8087 и дополняют его до 12 или 16 байтов, верно? - person Pascal Cuoq; 07.09.2013
comment
В моем случае 10 байт дополняются до 12, если я не скажу ему использовать настоящую библиотеку с четырехкратной точностью. Его sizeof() равен 16, но вы правы, он вполне может использовать только 10-байтовые числа с плавающей запятой. Но ДАЖЕ ЕГО ПЕРВЫЕ 10 БАЙТОВ НЕПРАВИЛЬНЫ. Заполнение не его проблема, его printme() не работает. - person Lee Daniel Crocker; 07.09.2013
comment
@LeeDanielCrocker Распечатка выглядит хорошо для меня: 128 бит от самого дальнего до ближайшего, начиная с 6 байтов заполнения, за которыми следует, соответственно, транскрипция с прямым порядком байтов, которая очень похожа на 80-битные представления 0.0 и 1.0. - person Pascal Cuoq; 07.09.2013
comment
@LeeDanielCrocker Я разместил код для printme(). Битовое представление мне подходит (см. en.wikipedia.org/wiki/Extended_precision) - person andreabedini; 07.09.2013
comment
@beb0s: С вашим printme все в порядке. Ли ошибается. - person R.. GitHub STOP HELPING ICE; 07.09.2013

Хотя стандарт C позволяет операциям стирать биты заполнения, я не думаю, что это происходит в вашей системе. Скорее, они никогда не инициализируются с самого начала, и GCC просто оптимизирует memset в -O1, так как объект впоследствии перезаписывается. Вероятно, это можно было бы подавить с помощью -fno-builtin-memset.

person R.. GitHub STOP HELPING ICE    schedule 07.09.2013
comment
Да, я согласен. Но впоследствии перезаписываются только незаполняющие биты объекта. Вы правы, -fno-builtin-memset решает проблему (в данном случае). - person andreabedini; 07.09.2013
comment
Концептуально запись в объект как long double записывает весь объект, но биты заполнения принимают неопределенное значение. Самое простое и эффективное неопределенное значение для реализации — это то, что уже есть. Однако, поскольку он не определен, компилятор может оптимизировать более ранние хранилища. - person R.. GitHub STOP HELPING ICE; 07.09.2013

Я не вижу здесь ничего неопределенного или даже неопределенного (две совершенно разные вещи). Да, вызовы memset() оптимизированы. На моей машине (i86-32) long double составляет 12 байт, дополненных до 16 в структурах и в стеке. На вашем компьютере они явно заполнены 16 байтами, так как sizeof(long double) возвращает 16. Ни один из выводов «printme» не похож на правильный 128-битный формат с плавающей запятой IEEE, поэтому я подозреваю, что в функции printme() есть другие ошибки, которые не показаны. здесь.

person Lee Daniel Crocker    schedule 07.09.2013
comment
Вывод printme показывает 80-битный расширенный двойник, который более или менее соответствует 79-битному формату IEEE 754, как описано здесь: en.wikipedia.org/wiki/Extended_precision - person Pascal Cuoq; 07.09.2013
comment
На вашем компьютере 12-байтовый long double — это 10 байтов данных и 2 байта заполнения. Посмотрите на LDBL_MAX из float.h. Или еще лучше, запустите эту программу с вашим компилятором: ideone.com/dY7ade - person Pascal Cuoq; 07.09.2013
comment
А... я вижу, что происходит. Он печатает байты в обратном порядке: сначала 6 байтов заполнения, затем 10 байтов значения от MSB до LSB, в отличие от того, как они хранятся. - person Lee Daniel Crocker; 07.09.2013