Pull to refresh

Comments 84

А зачем дизассемблировать? Ключик "-S" для gcc не подойдет в данном случае?
Тогда будет AT&T синтаксис. Вероятно, автору был ближе синтаксис Intel (как и мне, кстати).
В clang, кстати, не работает (там по-другому). Так что выполняемый следом objdump -d -Mintel универсальнее будет, если хочется синтаксиса Intel.
Мне тут мой знакомый напомнил, что число 1.005 точно не может быть представлено в формате с плавающей точкой (по стандарту IEEE 754), поэтому в памяти находится его приближение 1.00499… (здесь многоточие не означает иррациональность числа). После умножения получается 1004.99… Дальше уже ситуация складывается по-разному.
здесь многоточие не означает иррациональность числа


Так, на всякий случай: число, десятичная запись которого периодична, не может быть иррациональным. Вы имели в виду, что девятка у Вас образует период числа. Так это записывается несколько иначе, вот так: 1.004(9).
Девятка здесь не образует период числа. Многоточием я хотел показать, что просто опустил знаки. Кстати, по теории, если была бы 9 в периоде, было бы точно число 1005. Если переводить двоичное представление числа 1005 в десятичную систему счисления, то как раз так и выйдет. По форме записи будет получаться числовой ряд, а по значению — 9 в периоде.
Период может быть образован любой цифрой. Отсутствие 9 в периоде — это не более чем удобное соглашение, которое позволяет формально сделать соответствие между числами и их десятичной записью взаимно однозначным. В то же время девятка в периоде может в любой момент возникнуть при выполнении любых вычислений и для соблюдения соглашений надо просто каждый раз заменать (n) на n+1.

В Вашем случае девятка образует именно период, но он оказался обрезан из-за конечной точности представления. Если бы представление было точным, то действительно получилось бы равенство 1005, но из-за конечной точности период обрезается и получается погрешность в последнем разряде.

И уж никакой иррациональности (как Вы изволили написать) тут точно нет.
Конечно, нет. Просто с многоточием — это известная форма записи для иррациональных чисел. Точность представления числа определяется машинным эпсилоном.
Многоточие — известная форма записи любой последовательности.

Кстати, точность представления числа не имеет прямого отношения к машинному епсилон. Вульгарное отношение к машинному епсилон как к «единичке в младшем разряде» быстро пройдет после изучения Голдберга.
Любой бесконечной последовательности.
Вы не правы. Примерами являются арифметическая и геометрическая прогрессии.
Вам видимо действительно нужно повторить школьную программу, чтобы не путать последовательность и прогрессию. Ссылку-то прочитали? По определению любая последовательность содержит бесконечное число членов.
В математическом анализе конечные чп почти не рассматриваются, поэтому слово «бесконечные» опускается. Я Вам как математик говорю.
Я, батенька, тоже далеко не программист. Кстати, для математика странно ссылаться на ресурсы, содержащие в названии слова «основы высшей математики». Сошлитесь уж на что-нибудь авторитетное, типа Бурбаки.

Последовательность — отображение из множества целых чисел в произвольное множество. Все, точка, других определений в нормальной математике нет. В школьной математике всякое бывает, в терминологии она иногда довольно сильно отклоняется от той математики, которую используют математики.
Математики тоже разные бывают. Одни, например, не любят слово «кортеж» (сам видел). Тут надо шерстить ресурсы, очень не хочется. Работа с бесконечными чп прежде всего связана с определением предела. Конечные чп находят применение при описании конечных множеств. Чем прогрессия не последовательность? Есть понятие бесконечной геометрической прогрессии. Ещё раз: слово «бесконечная» очень часто опускают.
Естественное определение прогрессий определяет их именно как бесконечные последовательности. Если нужен отрезок прогрессии, то для этого есть готовое понятие сужения последовательности как функции (вробе бы иногда говорят об отрезке последовательности).
Здесь понятие арифметической прогрессии идёт как бесконечной чп.
Для Вас Александров Павел Сергеевич — авторитет? Для меня безусловно. Гляньте его книгу «Энциклопедия элементарной математики. Том III: функции и пределы». Параграф 34.
Вы правда думаете, что в ЭЭМ Александров написал хоть одну страницу? Я бы даже усомнился, в том что он ее просмотрел :) Все, что по названию напрямую связано с элементарной или школьной или «высшей» математикой доверия не вызывает по умолчанию.
Это у Вас доверия не вызывает. Да, я думаю, что Александров имел прямое отношение к этой книге. На конференциях встречал математиков, которые книги Бурбаки не любят.
Я ж не про доверие к Александрову (свою «теорию множеств и общую топологию» он писал сам и аккуратности определений там можно доверять, а ЭЭМ он редактировал и это две большие разницы.). И не про любовь/нелюбовь к Бурбаки. В «теории множеств и общей топологии» Александров тоже иногда уточняет слово «последовательность» словом «бесконечная». Возможно, что и там и в ЭЭМ это — артефакт устаревшей терминологии. Ведь терминология эволюционирует и употребление слова «последовательность» применительно к n-кам не является сейчас общепринятым среди математиков. Т.е. если Вы в статье назовете n-ку последовательностью, то Вас скорее всего поймут, но это вызовет недоумение. И в серьезных современных учебниках Вы такого тоже не найдете. Хорошо этот или плохо, но Бурбаки сейчас является общепринятым стандартом в смысле обозначений и терминологии.
Согласен по поводу серьёзных современных книг и статей по анализу — я тоже не встречал такого. И в какой-то из своих работ без оговорок писал последовательность, конечно, имея в виду бесконечную. В случае конечного числа членов по сути мы имеем конечное множество, а математика конечных множеств — это уже комбинаторика. Там, может быть, употребление термина «конечная последовательность» является самым обычным. Хотя тут я особо неспециалист. Кстати, в своей домашней библиотеке один томик Бурбаков имею («Функции действительного переменного»). :)
Один мне с пеной у рта доказывал, что «кортеж» — это неправильное слово в математике. :)) Я только улыбнуться могу.
Кортеж — это просто не получивший признания кривой термин. Большинство математиков говорят «n-ка».
В конечных последовательностях после многоточия указывается последний член. Кстати, здесь можно было бы написать 0 в конце записи числа.
В 2019 VS проблема не проявилась:

#include <stdio.h>
int main()
{
float a = 1.005f, b = 1000.0f;
int c = (int)(a * b);
printf("%d\n", c);
return 0;
}


выдает строго 1,005

проверка мантиссы:
#include <stdio.h>
int main()
{
float a = 1.0f, b = 1.0f, m = 10.0f;

int c = 1; while (a != a + (b /= m)) c++;

printf("%d\n", c);

return 0;
}


при м =10, выдает 8 десятичных знаков
при м =2, показывает стандарт, 24 бита
проверил для двух стандартов:

ISO C++17 Standard (/std:c++17)
ISO C++14 Standard (/std:c++14)
Заголовок, на мой взгляд, неверный. Поведение описано в стандартах, а первое называется знаковым сдвигом (с учетом знакового бита), а второе обьясняется неточностью представления дробных чисел в двоичном виде.

Кстати, вы еще можете получить очень даже целый ноль при вычитании близких чисел.
Не всё. Промежуточные «вычисления» производятся не в char'ах.
Cтандарт (c99) говорит нам про знаковый сдвиг следующее:
The result of E1 >> E2 is E1 right-shifted E2 bit positions.… If E1 has a signed type and a negative value, the
resulting value is implementation-defined.

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

Смотреть ассемблерный код в GCC стоит только в случае, если нужно изучить поведение именно на конкретном процессоре с конкретным набором инструкций. В отличие от проприетарных компиляторов, у GCC есть несколько десятков вариантов дампа в разных стадиях оптимизации: до удаления мёртвого кода, до свёртки констант, во внутреннем представлении GIMPLE и т. д. В данном случае, если скомпилировать с флагами -O0 -fdump-tree-optimized, то будет следующий вывод:
  a_1 = 1;
  _2 = (int) a_1;
  _3 = ~_2;
  _4 = _3 >> 1;
  b_5 = (unsigned char) _4;
  _6 = (int) b_5;
  printf ("%u\n", _6);


Касательно примера с float, все обобщения про C89-компиляторы и SSE — ваши личные измышления, не имеющие ничего общего с действительностью. В отсутствии требования на соответствие C89 стандарту IEEE 754 создатели GCC избрали стратегию свёртки констант с наибольшим соответствием текущей архитектуре. Для x86 это означает соответствие IEEE 754 на CPU, но не x87 FPU. Ещё раз: это выбор именно разработчиков GCC, причём конкретные усилия по соответствию IEEE 754 начали предприниматься только с четвёртой версии. В GCC 3.x свёртка констант работала в прямом смысле, как посчитает процессор, на котором работает GCC (независимо от целевой платформы). Другие C89-компиляторы могут иметь стратегию свёртки констант без потери точности или как GCC 3.x.
Тот ловкий момент, когда комментарий информативнее статьи! Спасибо.
Спасибо, хорошее дополнение к моему топику, списал себе Ваш совет.
Касательно ошибок компилятора — недавно имел возможность насладится багом оптимизатора AMD OpenCL при использовании операции бинарного или ;-) Баги бывают везде, но это конечно крайне редкая ситуация.
1.005 это бесконечная периодическая дробь в двоичной системе счисления, примерно равная 1.004999995 при обратном переводе из float. Соответственно после умножения на 1000 получается 1004.999995. По стандарту преобразование к целым отбрасывает дробную часть и мы получаем 1004. Так что ничего удивительного. Скорее уж необычно то, что SSE дает 1005 — но и это можно понять — видимо у GCC счет идет с двойной точностью и срабатывают чуть-чуть другие правила округления при неточном результате, из-за чего получается 1005 — так как при двойной точности округленное значение 1.005 = 1.0049999999999999.
Нет, это ясли.
В школе проходят чтение стандартов.
По комменту угадал школу, по нику подтвердил догадку)
gcc (SUSE Linux) 4.3.4 [gcc-4_3-branch revision 152973]
Мой gcc при компилляции без параметров выдал таки float 1.005*1000 = int 1005 — видимо «пофиксили багу» в 4.3.4 :)
Но double 1.005*1000 = int 1004, как и у вас, a float 1.004*1000 даёт int 1003.

Но я даже и не собираюсь ломать голову над этим — float к int кастовать себе дороже выйдет, используй округление :)
Это правильно, просто как-то попалась такая штука, я её и запомнил в свой «склерозник». По версии компилятора видно, что это было давно. :)
UFO just landed and posted this here
UFO just landed and posted this here
1. printf — это функция.
2. такое поведение возможно, если ваш код стардает неопределнным поведением. В таком случае, вам еще очень повезло, что вас не заблевали радугой единороги.
3. По секрету — gcc, Code:Blocks и даже CoIDE — почти одно и то же с точки зрения компилляции.
Минусуют за то, что не различаете компилятор и IDE.
И это, в CodeBlocks используется GCC. А значит сам редактор тут не при чем.
PS. я минусов не ставил.
Для работы с числами, ECMAScript использует стандарт IEEE 754:

The Number type has exactly 18437736874454810627 values, representing the double- precision 64-bit format IEEE 754 values as specified in the IEEE Standard for Binary Floating-Point Arithmetic, except that the 9007199254740990 distinct ―Not-a-Number‖ values of the IEEE Standard are represented in ECMAScript as a single special NaN value.


Поэтому, если выполнить в консоле браузера 1.005 * 1000, будет 1004.9999999999999
Где то было написано, что любые операции над числами приводят число к платформеному типу, во всяком случае для C# это точно указанно, что все операции для операндов меньше int приводятся к int.
Поймите простую вещь: в языке Си есть два базовых типа данных — int и void*, ну на некоторых жирных платформах ещё float и double. Всё остальное — даже не синтаксический сахар в современном понимании, а скорее костыли для тех мест, где это необходимо для правильного выбора компилятором ассемблерных инструкций, как в случае с unsigned или char. Раньше, собственно, даже указывать тип было необязательно, по умолчанию все переменные считались целыми.
Всё это сделано по той простой причине, что ни один процессор в мире, ну кроме Е2К, не знает, какой именно тип лежит у него, скажем, в аккумуляторе. Число и число, а что там подразумевает программист — вот пусть он сам себе и подразумевает.
В Си вы можете спокойно привести float* к int* или даже к int (*)(), и ни один компилятор в мире на это не ругнётся. По той простой причине, что, например, сравнивать float-ы с нулём, а на равенство — и друг с другом, быстрее через int*, чем через float*. Самомодифицирующийся код, как правило, сильно быстрее перегрузки простых виртуальных функций, особенно на старых процессорах. Скорость, скорость. Ну, а то, что можно прострелить себе ногу — так и на болиде Ф1 не проблема разбиться в первом же повороте.
Вы правы — это исторически, а сейчас на то и изобретают всякие , чтобы размерность была такой какой надо уж uint8_t чтоб был везде одинаков.
char, пожалуй, тоже очень даже «базовый тип». Стандарт значительно опирается на этот тип при описании процесса трансляции программ на C, операций со строками. В вычислениях, конечно, это не так ощутимо.
да, причем знаковый char, потому что ASCII7.
Не понял, если честно. Да, стандарт требует, чтобы используемый реализацией символьный тип поддерживал определённый набор символов, который составляет почти всю таблицу ASCII 7. Но вся эта таблица влезает как в знаковый, так и в беззнаковый восьмибитный тип, который реально чаще всего и используется (в том смысле, что в большинстве реализаций, если не во всех, CHAR_BIT == 8). Я уж не говорю о том, что signed char, char, unsigned char — три разных типа с точки зрения системы типов стандартного C. Некое специальное значение из этой троицы придаётся ещё типу unsigned char: массив unsigned char — это т.н. object representation [of value].

По какому признаку вы выделили signed char?
По той простой причине, что, например, сравнивать float-ы с нулём, а на равенство — и друг с другом, быстрее через int*, чем через float*.

Если мы ведем речь о стандартном Си, то то, что вы сейчас описали — это undefined behavior в чистом виде.

(начиная тупо с того, что у вас нет никаких гарантий, что sizeof(int) == sizeof(float))
В теории по стандарту — да, чистое UB. На практике на конкретных платформах скорость работы отличается в несколько раз.
Мне просто достоверно известно, что вот, например, gcc уже довольно давно активно использует оптимизации, которые основаны на предположении об отсутствии strict aliasing (т.е. на один объект не может быть два указателя разных типов) — и такими вот кастами можно огрести реальную проблему даже на совершенно тривиальном коде.

Вот конкретный пример, как раз с кастом float* к int*, хотя и в другом контексте:
thiemonagel.de/2010/01/no-strict-aliasing/

Поэтому делать такие штуки можно только в том случае, если компилятор вам гарантирует подобное поведение. А не просто когда оно вроде бы работает (а потом внезапно перестанет, потому что оптимизатор проснется).
На практике на конкретных платформах скорость работы отличается в несколько раз.

Тут бы ещё упомянуть Fast_inverse_square_root, вместе с оригинальным комментарием его автора.
Немного дополню Вас, так же автору статьи стоит посмотреть на ассемблерные команды, ну а если не очень тянет, то на C--, т.е. на язык наиболее близкий к железу:
Система типов в C-- преднамеренно спроектирована так, чтобы отображать ограничения, налагаемые аппаратным обеспечением компьютера, а не традициями систем типов более высокоуровневых языков. В C-- значение, хранимое в регистрах или памяти, может быть только одного типа — битовый вектор. Однако, битовый вектор является полиморфным типом и может быть разного размера: например, 8-битный или 32-битный. Кроме того, кроме битового вектора C-- также предоставляет булевский тип bool, который может быть типом результата выражения, а также может использоваться для управления выполнением программы и не может быть сохранён в регистре или в памяти.
Я в своё время хотел освоить Sphinx C--, руки так и не дошли. :(
Вот уж действительно парадоксы так парадоксы: расширение до целого при арифметических операциях и потеря точности чисел с плавающей запятой. Эти «парадоксы» обычно в любых книжках for dummies описываются по любому языку/платформе. Да и «программы на языке си и си++» тут непричём.
Об Integer Promotion ранее информацию не встречал.
Да ладно? И где это рассказывают сколько-нибудь подробно об арифметических операциях и не упоминают, что перед ними операнды расширяются до int (либо long в иных случаях)? Я понимаю, что можно в каком-нибудь хитром выражении частично «забыть» про это и попасть на какой-нибудь такой случай, но вот удивляться или называть это «парадоксом»…
Вы говорите о Implicit Type Conversion, т.е. что делает автоматически компилятор и что описано почти в любой книге, когда, например, у Вас есть код

char l;
int i;
...
i=l;

Здесь же идёт речь об Integer Promotion. Как я понимаю, это приведение целочисленных переменных к машинному слову, если эта переменная в памяти занимает меньше, чем слово. Для чего это нужно? На мой взгляд, эта штука нужна для эффективной работы арифметических и логических операций на ЦП.
Машинное слово здесь ни при чем, в стандарте четко прописано — промоутить до int.
Да, согласен (сейчас глянул стандарт), но в общем (с точки зрения языков программирования) эта ситуация, как мне думается, пусть косвенно, но связана с машинными словами.
Нет же, я имел в виду именно integer promotion операндов в выражениях, да вы про него сейчас сказали.
С одной стороны — согласен. С другой — если бы меня спросили, какой тип имеет ~a, я бы ответил, что тоже unsigned char. Мне почему-то казалось, что в случае с битовыми операциями promotion не делается. Типа раз вы всё равно шаманите на битовом уровне, то никаких расширений типов для хранения результатов. Так что статейка оказалась полезна.
Как я думаю, если собрать первую прогу как 64-е приложение, должно получаться приведение 64-ому слову => в ассемблерном коде мы должны увидеть регистр rax. Интересно, что в этом случае даст компиляция с флагами -O0 -fdump-tree-optimized?
Integer Promotion осуществляется к int (или к unsigned int, если продвигаемое значение имеет тип, все допустимые значения которого представимы в unsigned int, но не представимы в int), а не к машинному слову. Стандарт требует, чтобы в типе int были представимы значения от -216 до 216 — 1 (включительно). В реальности на популярных 32-битных платформах x86 все известные мне реализации отводят под int 4 байта — и ровно так же поступают на 64-битных платформах x86-64. Таким образом, на 64-битной платформе код будет вести себя так, словно продвижение осуществлялось к 4-байтовому целому типу. А какой в действительности регистр вы увидите в ассемблерном листинге, r*x или e*x, зависеть может ото всего, что угодно. Формально rax с нулями в старших 32 битам мало чем отличается от eax (в беззнаковом случае. В знаковом старшие 32 бита в арифметике с дополнением следует заполнять единицами). Поведение компилятора в этом отношении может измениться при смене местами пары строк в вашей программе, которая не меняет её семантику.
Более того, если вы считаете, что работа с 64-битными операндами на платформе x86-64 происходит быстрее (или так же быстро), как с 32-битными, или что 64-битный операнд для x86-64 является «родным», а 32-битный — «инородным», то скорее всего, вы заблуждаетесь. И вот почему: размер операнда «по умолчанию» для команд x86-64 32 бита, как и для команд x86. Это значит, что для расширения операнда до 64 бит перед инструкцией должен быть специальный REX-префикс. Да, xor rax, rax отработает по времени точно так же, как и xor eax, eax, да только сама инструкция длиннее:
   0:	48 31 c0             	xor    %rax,%rax
   3:	31 c0                	xor    %eax,%eax

Значит, код, использующий 64-битные операнды, объёмнее и больше нужно читать из памяти при его выполнении. А работа с памятью — бутылочное горлышко производительности современных x86-процессоров. Следовательно, если компилятор оптимизирующий, вы скорее всего не увидите rax в коде, сгенерированном для продвижения операндов бинарной операции.
Это не так: префиксом в x86-64 предваряются, наоборот, 32-битные операции.
godbolt.org в помощь.
Поведение от версии gcc зависит. У меня:

mick@mick-laptop:~$ gcc test4.c
mick@mick-laptop:~$ ./a.out
1005
mick@mick-laptop:~$ cat test4.c
#include <stdio.h>
int main()
{
float a = 1.005, b = 1000;
int c = a*b;
printf("%d\n", c);
return 0;
}
mick@mick-laptop:~$ gcc -v
Используются внутренние спецификации.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/4.6/lto-wrapper
Целевая архитектура: x86_64-linux-gnu
Параметры конфигурации: ../src/configure -v --with-pkgversion='Ubuntu/Linaro 4.6.3-1ubuntu5' --with-bugurl=file:///usr/share/doc/gcc-4.6/README.Bugs --enable-languages=c,c++,fortran,objc,obj-c++ --prefix=/usr --program-suffix=-4.6 --enable-shared --enable-linker-build-id --with-system-zlib --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --with-gxx-include-dir=/usr/include/c++/4.6 --libdir=/usr/lib --enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --enable-gnu-unique-object --enable-plugin --enable-objc-gc --disable-werror --with-arch-32=i686 --with-tune=generic --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu
Модель многопоточности: posix
gcc версия 4.6.3 (Ubuntu/Linaro 4.6.3-1ubuntu5)
В С/С++ очень много зависит от компилятора и целевой архитектуры. Когда-то читал статью на RSDN «Занимательный С++», как раз по теме…
Sign up to leave a comment.

Articles