> Если программист прибавляет единицу к unsigned int, а потом смотрит, не получился ли 0 — формально он неправ
Именно для unsigned правила выглядят так:
C99 пункт 6.2.5.9:
>> A computation involving unsigned operands can never overflow, because a result that cannot be represented by the resulting unsigned integer type is reduced modulo the number that is one greater than the largest value that can be represented by the resulting type.
Аналогичное правило в C++11, пункт 3.9.1.4:
>> Unsigned integers, declared unsigned, shall obey the laws of arithmetic modulo 2n where n is the number of bits in the value representation of that particular size of integer.
А вот для signed они одинаково (косвенно) определяют, что переполнение — UB.
Как результат, можно проверять переполнение (сильно менее эффективно, чем напрямую машинными средствами) через перевод в unsigned и изучение результата; и точно так же можно реализовывать «заворачивающуюся» арифметику.
С другой стороны, я согласен с общей идеей. В результате подобного подхода со стороны стандартизаторов и авторов компиляторов, поворачивающих «закон» в свою сторону, я уже слышал много сообщений типа «ну вас нафиг, ухожу на Java/C#/Go/etc.» именно за счёт гарантий, которые даёт эта группа; часто их даже не интересует managed memory — их задалбывает мир, где любой неосторожный шаг приводит к падению в пропасть.
> поэтому мне бы казалось разумным многие случаи undefined behavior перевести в unspecified behavior или в implementation defined.
Это сомнительный плюс. По крайней мере в той версии, в которой в Go:
>> When the input is broken into tokens, a semicolon is automatically inserted into the token stream immediately after a line's final token if that token is
>> an identifier
>> an integer, floating-point, imaginary, rune, or string literal
>> one of the keywords break, continue, fallthrough, or return
>> one of the operators and punctuation ++, --, ), ], or }
В результате я могу, например, написать
x1 := (-b + dd) /
(2 * a)
но не могу
x1 := (-b + dd)
/ (2 * a)
Для сравнения, в Python правило выглядит так — он продолжает «подбирать» следующую строку, если есть незакрытая скобка любого вида, или если текущая строка заканчивается обратной косой:
x1 = (-b + dd) \
/ (2 * a)
или с незакрытой скобкой
x1 = ((-b + dd)
/ (2 * a))
Обычно таки языки, в которых есть автотерминация statementʼа завершением строки, дают средства продолжения. Python — показано выше. BASIC (Microsoft) — '_' в конце. Fortran с fixed format — не пробел и не '0' в 6-й позиции следующей строки, с free format — '&' в конце предыдущей строки. И так далее.
В Go же получается, по факту, дополнительное принуждение к конкретному стилю. Не то чтобы это выходило за общие рамки — там таких принуждений вагон и маленькая тележка, и они громко выдаются за преимущества языка — но всё равно выглядит насилием.
Точно то же с '{' для if, for — принудительно на той же строке, что условие. Это было понятно в Tcl, где тело в {...} — часть строки с особым режимом квотинга, но не в языке, где нет ограничения единственным типом данных «строка».
И, самое существенное, непонятно, чего они боялись тут. В Си может быть проблема типа такого, что незакрытая через ';' сущность будет дополнена следующим #include — такие проблемы иногда можно долго искать. А в Go что?
Ну и, очевидно, нежелание ';' — вопрос откровенной вкусовщины. Меня это не пугает (после Fortran, Python, shell и прочих), но и считать откровенным преимуществом — согласиться сложно. Не так уж сложно было их ставить :)
> Строгая типизация не даёт наделать ошибок.
Если сравнивать с каким-нибудь Javascript или PHP, то да. Если с более близкими языками, как C, то преимущества более сильной типизации откровенно не раскрыты.
Например, нет неявной конверсии int -> int32 и назад, даже если int 32-битный. Нет неявного сужения типа int32 -> int8. Но где удобные операторы конверсии с проверкой? Как мне проверить, что число влезло в int8 без потерь — конвертировать снова в int32 и сравнивать с исходным?
Нет неявной конверсии int -> float. OK, может, это даже полезно (на общепринятых размерах, int -> float это возможная потеря точности, в отличие от int -> double). Как мне провести конверсию с конкретным округлением и проверкой отсутствия реального округления? Тоже сравнивать результат обратной конверсии?
Зато хохмы типа типизированного nil и неожиданного nil != nil (если они ожидаются разных типов) — откровенная диверсия.
> При заполнение структуры можно вставить и вызов функции и какой то расчёт.
А где нельзя (и это настолько проблема)?
> Нет классического ООП. Зато есть простые механизмы его заменяющие.
Наследование (реализации) таки не заменяется. И как бы его ни ругали, есть масса случаев, когда оно полезно и удобно. Написание аналогичного в языке без наследования реализации превращается в рисование затычек и повтор кода.
> switch по string.
Это настолько важно?
> Функция может возвращать несколько значений
Да. Но уже давно не уникально.
> Горутины. Просто и удобно.
Нет, если мы выходим за пределы стандартных примеров :)
Как только начинаются проблемы типа «вот этому пулу действий дать гарантированные 40% процессора», реальный мир врывается и всё ломает. Сразу начинаются пляски с sync.* и тому подобными ужасами…
или каналы с особенностями…
> Есть только один for на все случаи жизни
Ну это совсем ничтожное преимущество — ещё и сбивающее с толку.
Если сравнивать, то вот что я вижу положительного в Go:
1. select из каналов, вместе с самими каналами как средством взаимодействия. Это преимущество, в первую очередь, по сравнению с Erlang.
2. Принудительность {} вокруг тел if, for. Мы имеем это в своём стиле для C/C++/Java/etc., и это сильно помогает, но есть места, где настаивают на обратном для однооператорных тел (например, FreeBSD, Linux kernel) и регулярно ловят проблемы от этого. Преимущество не уникальное (см. хотя бы Swift). Редкий случай, когда форсирование стиля идёт на безусловную пользу.
3. Паскалеподобный порядок в декларациях (var x[:] int, а не int x) — упрощает понимание сложных случаев (не буду приводить хрестоматийные садистские примеры для C).
4. Жёстко определённые правила исполнения арифметики — дополнительный код с сохранением младших бит для +-*, выполнение сдвигов на всю ширину заданного сдвига. Никаких тебе «undefined behavior» стиля C, которые бьют по голове из-за угла даже очень опытным программистам. Иногда и более жёсткое (например, порядок вычисления аргументов функции) — сомнительно, но позволяет предсказывать побочные эффекты.
Тут хотелось бы иметь контекстное ослабление подобных правил (и бо́льшую свободу) для варианта «да, автор уверен», вплоть до того, как в C, и свободы оптимизации от этого — но, видно, не входит в представления о целевой группе.
5. Устранение некоторых наследственных кривостей C типа что '&' приоритет ниже, чем у арифметики (кто на это не нарывался — считает &, &&, *, / примерно одним уровнем — и удивляется последствиям). Но не всех кривостей :(
6. Определение переменных для контекста if, for в его входном условии (позднее перенесено в C++17).
7. defer (я не про panic/recover, а про defer вообще). Идея старая, но на уровне языка вроде только в Go (boost::scope надо ещё подключать).
Гуглоэффект генерации рекурсии: что бы там ни было вначале, но сейчас для этой фразы поиск выдаёт три ссылки, которые все на хабр с данной статьёй, и больше ничего.
Когда-то уже сталкивался с этим…
Во всех основанных на gtk приложениях (включая Firefox и Chrome) работает Ctrl+Shift+U для ввода любого Unicode символа шестнадцатеричным кодом.
Для консоли — в screen есть digraph U+, который далее хочет 4 16-ричных символа и выдаёт UTF-16 код. А именно ° вводится там как диграф ~o.
Ну и всякие kcharselect и аналоги (громоздко, но доступно).
> В double такого эффекта нет. Там, что 0.1 прибавить, что 100 — если точности не хватило, то число просто увеличено не будет.
Хм, не согласен. Поведение decimal в этом смысле ничем не отличается от поведения плавучки IEEE (тут и далее — для дефолтного режима округления, roundToNearestTiesToEven).
Возьмём для примера single, чтобы не было слишком больших степеней. Множество представимых в нём чисел это «сетка» с шагом, который меняется в 2 раза при переходе через степень двойки. Например, самое большое представимое число это 2^128-2^104; на одну позицию ниже 2^128-2*2^104, затем 2^128-3*2^104, и так далее по убыванию до 2^127; от 2^127 вниз до 2^126 шаг 2^103; и так далее. Следующая возможная точка в сторону увеличения была бы 2^128, но она уже непредставима. Результат любой операции округляется в сторону ближайшей представимой точки, а если точно посредине — выбирается та, которая более чётная (voting digit — минимальная цифра из представимых в округлённом результате — равна 0).
Так вот — если Вы к этому MAX_SINGLE = 2^128-2^104 ~= 3.40282347E+38 прибавите любое положительное число меньше чем 2^104, округлённая сумма будет снова равна MAX_SINGLE, потому что точная сумма меньше среднего между MAX_SINGLE и 2^128; но если прибавите любое число не меньшее чем 2^104, точная сумма будет уже не меньше этого среднего, поэтому сработает округление к ближайшему чётному, оно будет равно 2^128, не представимо в single, и результатом будет возбуждение исключения переполнения (а если исключение заглушено, будет выдан результат +INF). (В стандарте это правило сказано другими словами, но мне описание через следующую возможную точку кажется более естественным.)
Та же логика, с поправкой на размеры мантиссы и диапазоны порядка Decimal, применяется и к нему. «The finite set of values of type Decimal are of the form m / 10^e, where m is an integer such that -2^96 < m < 2^96, and e is an integer between 0 and 28 inclusive.» Шаг между значениями около Decimal.MAX_VALUE равен 1 (e == 0), и первое непредставимое — следующее за MAX_VALUE — равно MAX_VALUE+1 == 2^96. Прибавка к MAX_VALUE любого положительного меньше 0.5 игнорируется, а >=0.5 — даёт округление к 2^96, которое уже не влазит => генерируется исключение переполнения. Тесты это подтверждают.
Ну почему же зло? Вполне добро, просто не для бухгалтерии. :)
Для матфизики, наоборот, фиксированная десятичная точка будет бессмысленной.
Вот то, что в C, C++, C# плавучка обязательна, а фиксированная точка — нет, уже показывает ориентацию языков. А в результате имеем сохранение старого кода на всяких коболах ;(
Эффект сам по себе банальный и общеизвестный, в инструкциях по работе с плавучкой он описывается одним из первых. Речь не о нём в принципе, а о том, что (1) в бухгалтерии он недопустим, и (2) защита в .NET Decimal от него есть только в виде «а вы не допускайте таких больших чисел».
то видно, что Math.Round ничего не изменил, а вот дальше возникает отклонение (но рантайм врёт в печати — проверено на Mono 4.6.2 и .NET Core 1.0.0-preview2-003118, результаты идентичны):
Для сравнения то же на Python3 (идеально честный вывод: он для любой заданной точности подбирает десятичную строку такую, что обратно превращается в такую же двоичную) —
Первый пункт я бы формулировал немного иначе, хотя по сути близко.
Идти в бухгалтерских расчётах (речь о них — не о финансовых в целом, там может быть и «высокая» математика) надо от некоторых базовых правил. В первую очередь это то, что деньги не могут появляться ниоткуда и исчезать в никуда, любая сумма и разность должны быть точными. Как бы ни были сформулированы правила для округлений в каком-то случае (НДС, проценты, другое), это особые ситуации. Лучше получить исключение переполнения, если точности не хватает, чем потерять доли (копейки) и потом искать источник несведения баланса.
Основной проблемный случай — деление на несколько частей. Например, деля 100.00 поровну на 3, мы должны получить не 33.33+33.33+33.33, а 33.33+33.33+33.34. Кому будет лишняя копейка — вопрос полиси, но она не должна потеряться неизвестно где. Аналогично, деля даже на 2: 2.33 пополам должно дать 1.16+1.17, а не 1.16+1.16, как получилось бы, если бы округляли оба результата деления по умолчанию. Проценты? Пожалуйста — 6.99 при выделении шестой части должно дать 1.16+5.83 или 1.17+5.82, но не 1.16+5.82.
Далее. Чем плоха плавучка? И двоичная, и десятичная тут плохи. Для двоичной — банальный пример: в IEEE double, 0.1*3 — 0.3 равно 5.55...e-17. Часто достаточно того, что это не 0: чтобы сравнить с нулём, надо вначале округлить до 0.01 (или какая там выбрана точность). Вообще, все расчёты, получается, надо вести так, что вместо a+b мы делаем round(round(a,0.01), round(b,0.01), 0.01) (вторым аргументом я показал единицу точности). Но так как round(a,b) обычно выполняется как b*round_int(a/b), то резонный вопрос — нафига вообще держать числа в дробном виде, если на каждую операцию они переводятся в целые, а потом обратно?
Есть десятичная плавучка — даже в IEEE754 (версии 2008 года), IBM её умеет аппаратно, многие остальные — программно (например, GCC даёт для C). Но и с ней проблемы. Decimal32 из IEEE это 7 точных десятичных цифр. Сложив 9999999 и 4, мы получим 10000000, а не 10000003, точности для последней цифры не хватит. И потеря будет молчаливой, за исключением (обычно замаскированного) inexact exception. С такими данными можно работать, но только при условии, что inexact exception всегда включен или регулярно проверяется, кроме отдельных операций, где он вреден (вдруг кому-то таки нужен квадратный корень из суммы). Более того, после операций за пределами обычного сложения/вычитания надо округлять в нужные пределы. Если 19.5% от 123.45, пусть мы даже подсчитаем проценты в float (любом), результат должен быть выдан уже как 23.46, а не 23.4555, и только этот результат имеет право быть назван суммой в выбранной валюте.
Аналогично проблемен и Decimal из C#, и даже больше: если каким-то образом вылезли за его 28 цифр, то там даже inexact exception не будет: он не умеет жаловаться на такое. Поэтому вернуться к сумме после всех вычислений надо через Round(), не доверяя любым предыдущим результатам. Но там сложно найти случай переполнения этих 28 цифр :) и это спасает на практике. Но, боюсь, не все сертификаторы положительно оценят подобный «авось» даже тогда, когда суммы в септиллионы букозоидов не бывают в принципе: контроля переполнения и потери точности нет => не годится.
Поэтому основной вариант, о котором надо думать — это масштабированные целые. Где конкретно кому — целые копейки, 1/100 цента, как-то ещё — это уже по местным требованиям.
Далее, это было только про сложение/вычитание чисел одной размерности. Но есть и другие случаи. А именно:
* одна валюта, но разные размерности (где-то копейки, где-то сотые доли копейки)
* разные валюты и деноминации (автор поста достаточно изложил, не повторяю)
* иные операции, чем сложение и вычитание однородных денег
и вот тут действительно очень на пользу иметь обёртку, которая не позволяет просто так сложить, например, деньги США в сотых долях цента с деньгами Беларуси в копейках; и конверсия каждый раз должна быть явной (с возможным указанием метода округления); никаких автоматических сложений, например, копеек с сотыми долями копейки, даже если машина знает, что валюта одинаковая.
Операции умножения на целое ещё могут проходить в том же логическом «поле» сумм, деление — уже нет (см. примеры выше), аналогично взятие процентов. Более сложная арифметика и алгебра изначально должны выполняться в пределах подходов с плавающей точкой, хотя, возможно, с расширенной точностью (single, он же float в C, очень часто недостаточен; double уже обычно годится). Возврат в финансы — только через приведение к соответствующему типу с указанием выходной точности и контролем особых входных данных типа INF или NaN.
К остальному есть немного комментариев.
> Сумма не может быть отрицательной
Даже пытался возразить по мелочам, но не смог :) на самом деле часто допускают временный уход какого-то счёта в минус; проблема возникает, когда этот минус не закрыт на какой-то момент (как конец рабочего дня). Не могу сказать, что это хорошо, но это явно не всегда плохо.
Показ переплаты как долга с минусом — да, против не готовых к такому может быть диверсионным. Не зря в некоторых традициях вместо минуса используют другую нотацию — например, [в квадратных скобках]. Хотя сейчас эти скобки, наоборот, собьют технически подготовленных.
Вот противоположный стиль — переплата с плюсом, а долг с минусом — меньше сбивает, насколько я видел; особенно если потом таки выписать «к оплате столько-то» и писать ноль в случае переплаты.
> Поэтому лучше всего использовать нейтральные понятия «сделка», «обмен», «дебет», «кредит», «зачисление», «списание» и т. д.
Это ещё менее понятно, just IMHO. Подозреваю, хоть как-то универсально понятным будут варианты типа «сторона A получает...», «сторона B получает...»
> Суммы округлять через round() перед каждой операцией — далеко не самый плохой приницп работы с ними.
Вопрос: а зачем? Вычисления в целых числах заведомо не имеют подобной проблемы и обходятся обычно дешевле (сумма целых даже в виде Decimal из C# проще, чем два round плюс суммирование и масштабирование).
> Как вариант: хранить суммы в полях int в значениях наименьшой возможной единицы (копейки, центы) и делить их на степень кратности к денежной единице только перед выводом на экран/печать. Кстати, такие реализации встречал неоднократно.
Именно. Фактически, для целей финансов это самый нормальный режим. (С поправкой: не только печать, но и всякая конверсия; иногда и в какой-нибудь float надо переводить — например, проценты взять.)
> Покупка/продажа валюты, акций и т.д. во всем мире идет как bid/ask. Оператор рынка (банк в данном случае) объявялет обменные курсы, по которым покупает и продает. Если юзер не понимает таких элементарных вещей, то лучше ему не заниматься такими операциями.
Практически любой человек, который ездит за границу, вынужден думать о покупке/продаже валюты. Предлагаете вообще не ездить?
> но если функция выполняется в цикле — вполне может её векторизовать
Случай векторизации я тут не рассматривал. Вообще у нас дискуссия как-то странно идёт — одни и те же аргументы применяются несколькими сторонами то в контексте конкретного примера, то в контексте высокоскоростного кода, который надо писать, чтобы компилятор его оптимизировал и векторизовал… половина путаницы из-за этого.
> используются на несколько порядков чаще, чем вещи типа кодеков
Ну вот моя специфика тут пары последних лет это где-то «кодеки». Параллельности немного (ещё и навязанные средства мешают), а вот путаницы в форматах разборе данных ой-ой. Пока что по результатам этой дискуссии пришлось добавить -fno-strict-aliasing на пару исходников, причём я подозреваю, что если я туда повлепливаю memcpy, то меня не поймут кое-какие коллеги.
> GPU — это хороший пример. Ни один GPU не даёт вам возможности «штатно» программировать его на ассемблере.
И снова — я имел в виду вообще переписывать код, а Вы вдруг ассемблер вспомнили :) почему?
> Кармак отлично знает ассемблер
> однако в данном конкретном случае он-таки предпочёл остаться в рамках C.
А вот тут уже очень интересный момент. Если он «предпочёл остаться в рамках C» (за это Вы его хвалите), но применил вместо разрешённого подхода — трюк, про который говорите «никогда-никогда!» (и который не был допустим даже по C89, насколько я нагуглил), о чём это говорит? Он был прав или нет?
> Ну или можно было бы пойти на UB, рискнуть непереносимостью — и создать тот Quake, которым он был создан.
Авторы Quake, я предполагаю, вообще не озадачивались подобным выбором, зная свою целевую обстановку. Агрессивные оптимизации, которые могли бы испортить такой код, тогда не применялись ни одним компилятором, доступным для Windows; тогда только-только начинала появляться теория таких оптимизаций. Например, книга Мучника — одно из классических изданий такого рода — это 1997-й, но реальное появление чего-то такого в доступных компиляторах это уже 2000-е. Формализация проблемы это тоже стандарт C99, не раньше(?)
Зная, что тут проблемы нет, авторы Quake могли применять подобные фишки в полный рост, не опасаясь последствий.
Кроме того, сам факт бинарной поставки мог позволить доточить по вкусу, где нужно, даже после компилятора :)
А вот для современного применения, с этим образцовым кодом уже проблемы. И если то, что коллега Halt взвился на этот код по нынешним меркам, было просто реакцией на персональную разновидность красной тряпки (у кого что болит...), и за пределами основной темы статьи, то Ваше предложение проверить на юнит-тестах, увы, совсем некорректно.
> И получили бы замедление раз в два (хорошего если в 10) через несколько лет при смене архитектуры процессора.
На movd с аналогом? Это что же должно случиться, чтобы тут возникло замедление в 10 раз?
> вы считаете что на каждый критичный участок нужно посадить программиста, который будет следить за изменениями в мире процессоров и постоянно подкручивать ваш код?
Я не «считаю», я вижу, что оно сейчас именно так и происходит. Каждый год от смены чего-то «утекают» как минимум несколько процентов от результата. Иногда — в разы (как с приходом GPU).
Совершенно удивительная статья — огромное количество слов, но нигде не сказано про проблему лицензии клиента БД.
Почему она важна? Потому что, по тому же процитированному автором статьи:
> свобода номер 0: возможность запускать программу с любой целью
для СУБД (как и любого сетевого сервера) это означает, что она может использоваться клиентскими программами любой лицензии — аналогично тому, как программа под GPL может использоваться под управлением сколь угодно проприетарного комплекта софта, взаимодействуя с последним через штатные каналы связи, через файлы и т.д.
Но СУБД имеют свой протокол. Для MySQL лицензия утверждает совместимость GPL-версии штатного клиента только с явным и закрытым (нерасширяемым) списком FOSS-лицензий. (Список, кстати, странный — в нём нет даже GPLv2.) Коммерческий продукт обязан брать коммерческую лицензию или использовать отдельный процесс-переходник (по сути, последний вариант — хак для лицензии). (Разумеется, есть альтернативные клиентские библиотеки. Но это таки другой продукт.)
Этого для меня, как и, думаю, для большинства потенциальных разработчиков программ — клиентов БД, достаточно, чтобы считать MySQL несвободным: его свобода ограничивает чужую свободу в тех местах, где штатно предполагается отсутствие ограничительных рамок на чужие лицензии.
И вот тут PQ отличается — лицензия клиента по сути тождественна BSD или MIT и не ограничивает лицензирование того, кто использует штатную реализацию клиента.
Не хочу тут раздувать новый спор про уже известные проблемы, потому что они уже обсуждены, кажется, чуть менее чем всеми, за последние 10-15 лет: это темы GPLv3 vs. GPLv2, тема свободы разработчиков против свободы пользователей и частный случай GPL vs. BSD в вопросах построения не-FOSS продуктов на базе FOSS. Ищущий да обрящет. Мой посыл был в том, что статья подобного рода, как тут, без упоминания той тематики, что я назвал — бессмысленна и вводит в заблуждение. Спасибо за внимание.
Разработчиков компиляторов, или целевых программ — их пользователей?
Если второе, то в этой дискуссии достаточно примеров, и в реале вокруг меня.
Именно для unsigned правила выглядят так:
C99 пункт 6.2.5.9:
>> A computation involving unsigned operands can never overflow, because a result that cannot be represented by the resulting unsigned integer type is reduced modulo the number that is one greater than the largest value that can be represented by the resulting type.
Аналогичное правило в C++11, пункт 3.9.1.4:
>> Unsigned integers, declared unsigned, shall obey the laws of arithmetic modulo 2n where n is the number of bits in the value representation of that particular size of integer.
А вот для signed они одинаково (косвенно) определяют, что переполнение — UB.
Как результат, можно проверять переполнение (сильно менее эффективно, чем напрямую машинными средствами) через перевод в unsigned и изучение результата; и точно так же можно реализовывать «заворачивающуюся» арифметику.
С другой стороны, я согласен с общей идеей. В результате подобного подхода со стороны стандартизаторов и авторов компиляторов, поворачивающих «закон» в свою сторону, я уже слышал много сообщений типа «ну вас нафиг, ухожу на Java/C#/Go/etc.» именно за счёт гарантий, которые даёт эта группа; часто их даже не интересует managed memory — их задалбывает мир, где любой неосторожный шаг приводит к падению в пропасть.
> поэтому мне бы казалось разумным многие случаи undefined behavior перевести в unspecified behavior или в implementation defined.
+100.
Это сомнительный плюс. По крайней мере в той версии, в которой в Go:
>> When the input is broken into tokens, a semicolon is automatically inserted into the token stream immediately after a line's final token if that token is
>> an identifier
>> an integer, floating-point, imaginary, rune, or string literal
>> one of the keywords break, continue, fallthrough, or return
>> one of the operators and punctuation ++, --, ), ], or }
В результате я могу, например, написать
но не могу
Для сравнения, в Python правило выглядит так — он продолжает «подбирать» следующую строку, если есть незакрытая скобка любого вида, или если текущая строка заканчивается обратной косой:
или с незакрытой скобкой
Обычно таки языки, в которых есть автотерминация statementʼа завершением строки, дают средства продолжения. Python — показано выше. BASIC (Microsoft) — '_' в конце. Fortran с fixed format — не пробел и не '0' в 6-й позиции следующей строки, с free format — '&' в конце предыдущей строки. И так далее.
В Go же получается, по факту, дополнительное принуждение к конкретному стилю. Не то чтобы это выходило за общие рамки — там таких принуждений вагон и маленькая тележка, и они громко выдаются за преимущества языка — но всё равно выглядит насилием.
Точно то же с '{' для if, for — принудительно на той же строке, что условие. Это было понятно в Tcl, где тело в {...} — часть строки с особым режимом квотинга, но не в языке, где нет ограничения единственным типом данных «строка».
И, самое существенное, непонятно, чего они боялись тут. В Си может быть проблема типа такого, что незакрытая через ';' сущность будет дополнена следующим #include — такие проблемы иногда можно долго искать. А в Go что?
Ну и, очевидно, нежелание ';' — вопрос откровенной вкусовщины. Меня это не пугает (после Fortran, Python, shell и прочих), но и считать откровенным преимуществом — согласиться сложно. Не так уж сложно было их ставить :)
> Строгая типизация не даёт наделать ошибок.
Если сравнивать с каким-нибудь Javascript или PHP, то да. Если с более близкими языками, как C, то преимущества более сильной типизации откровенно не раскрыты.
Например, нет неявной конверсии int -> int32 и назад, даже если int 32-битный. Нет неявного сужения типа int32 -> int8. Но где удобные операторы конверсии с проверкой? Как мне проверить, что число влезло в int8 без потерь — конвертировать снова в int32 и сравнивать с исходным?
Нет неявной конверсии int -> float. OK, может, это даже полезно (на общепринятых размерах, int -> float это возможная потеря точности, в отличие от int -> double). Как мне провести конверсию с конкретным округлением и проверкой отсутствия реального округления? Тоже сравнивать результат обратной конверсии?
Зато хохмы типа типизированного nil и неожиданного nil != nil (если они ожидаются разных типов) — откровенная диверсия.
> При заполнение структуры можно вставить и вызов функции и какой то расчёт.
А где нельзя (и это настолько проблема)?
> Нет классического ООП. Зато есть простые механизмы его заменяющие.
Наследование (реализации) таки не заменяется. И как бы его ни ругали, есть масса случаев, когда оно полезно и удобно. Написание аналогичного в языке без наследования реализации превращается в рисование затычек и повтор кода.
> switch по string.
Это настолько важно?
> Функция может возвращать несколько значений
Да. Но уже давно не уникально.
> Горутины. Просто и удобно.
Нет, если мы выходим за пределы стандартных примеров :)
Как только начинаются проблемы типа «вот этому пулу действий дать гарантированные 40% процессора», реальный мир врывается и всё ломает. Сразу начинаются пляски с sync.* и тому подобными ужасами…
или каналы с особенностями…
> Есть только один for на все случаи жизни
Ну это совсем ничтожное преимущество — ещё и сбивающее с толку.
Если сравнивать, то вот что я вижу положительного в Go:
1. select из каналов, вместе с самими каналами как средством взаимодействия. Это преимущество, в первую очередь, по сравнению с Erlang.
2. Принудительность {} вокруг тел if, for. Мы имеем это в своём стиле для C/C++/Java/etc., и это сильно помогает, но есть места, где настаивают на обратном для однооператорных тел (например, FreeBSD, Linux kernel) и регулярно ловят проблемы от этого. Преимущество не уникальное (см. хотя бы Swift). Редкий случай, когда форсирование стиля идёт на безусловную пользу.
3. Паскалеподобный порядок в декларациях (var x[:] int, а не int x) — упрощает понимание сложных случаев (не буду приводить хрестоматийные садистские примеры для C).
4. Жёстко определённые правила исполнения арифметики — дополнительный код с сохранением младших бит для +-*, выполнение сдвигов на всю ширину заданного сдвига. Никаких тебе «undefined behavior» стиля C, которые бьют по голове из-за угла даже очень опытным программистам. Иногда и более жёсткое (например, порядок вычисления аргументов функции) — сомнительно, но позволяет предсказывать побочные эффекты.
Тут хотелось бы иметь контекстное ослабление подобных правил (и бо́льшую свободу) для варианта «да, автор уверен», вплоть до того, как в C, и свободы оптимизации от этого — но, видно, не входит в представления о целевой группе.
5. Устранение некоторых наследственных кривостей C типа что '&' приоритет ниже, чем у арифметики (кто на это не нарывался — считает &, &&, *, / примерно одним уровнем — и удивляется последствиям). Но не всех кривостей :(
6. Определение переменных для контекста if, for в его входном условии (позднее перенесено в C++17).
7. defer (я не про panic/recover, а про defer вообще). Идея старая, но на уровне языка вроде только в Go (boost::scope надо ещё подключать).
Остальное или так же, как у аналогов, или кривее.
Когда-то уже сталкивался с этим…
Для консоли — в screen есть digraph U+, который далее хочет 4 16-ричных символа и выдаёт UTF-16 код. А именно ° вводится там как диграф ~o.
Ну и всякие kcharselect и аналоги (громоздко, но доступно).
Поправка — 2^103, ибо как раз половина интервала до следующего; то же самое дальше по абзацу.
Хм, не согласен. Поведение decimal в этом смысле ничем не отличается от поведения плавучки IEEE (тут и далее — для дефолтного режима округления, roundToNearestTiesToEven).
Возьмём для примера single, чтобы не было слишком больших степеней. Множество представимых в нём чисел это «сетка» с шагом, который меняется в 2 раза при переходе через степень двойки. Например, самое большое представимое число это 2^128-2^104; на одну позицию ниже 2^128-2*2^104, затем 2^128-3*2^104, и так далее по убыванию до 2^127; от 2^127 вниз до 2^126 шаг 2^103; и так далее. Следующая возможная точка в сторону увеличения была бы 2^128, но она уже непредставима. Результат любой операции округляется в сторону ближайшей представимой точки, а если точно посредине — выбирается та, которая более чётная (voting digit — минимальная цифра из представимых в округлённом результате — равна 0).
Так вот — если Вы к этому MAX_SINGLE = 2^128-2^104 ~= 3.40282347E+38 прибавите любое положительное число меньше чем 2^104, округлённая сумма будет снова равна MAX_SINGLE, потому что точная сумма меньше среднего между MAX_SINGLE и 2^128; но если прибавите любое число не меньшее чем 2^104, точная сумма будет уже не меньше этого среднего, поэтому сработает округление к ближайшему чётному, оно будет равно 2^128, не представимо в single, и результатом будет возбуждение исключения переполнения (а если исключение заглушено, будет выдан результат +INF). (В стандарте это правило сказано другими словами, но мне описание через следующую возможную точку кажется более естественным.)
Та же логика, с поправкой на размеры мантиссы и диапазоны порядка Decimal, применяется и к нему. «The finite set of values of type Decimal are of the form m / 10^e, where m is an integer such that -2^96 < m < 2^96, and e is an integer between 0 and 28 inclusive.» Шаг между значениями около Decimal.MAX_VALUE равен 1 (e == 0), и первое непредставимое — следующее за MAX_VALUE — равно MAX_VALUE+1 == 2^96. Прибавка к MAX_VALUE любого положительного меньше 0.5 игнорируется, а >=0.5 — даёт округление к 2^96, которое уже не влазит => генерируется исключение переполнения. Тесты это подтверждают.
Для матфизики, наоборот, фиксированная десятичная точка будет бессмысленной.
Вот то, что в C, C++, C# плавучка обязательна, а фиксированная точка — нет, уже показывает ориентацию языков. А в результате имеем сохранение старого кода на всяких коболах ;(
Я не о переполнении порядка, а о превышении точности представления (и Вы пропустили слово inexact — это не overflow):
decimal d2 = 1.0e28m;
decimal d3 = 0.1m;
decimal d4 = d2 + d3;
Console.WriteLine("{0:F40}", d4);
Console.WriteLine("{0:F40}", d4 - d2);
получаем:
d3 тупо ушло в никуда.
Эффект сам по себе банальный и общеизвестный, в инструкциях по работе с плавучкой он описывается одним из первых. Речь не о нём в принципе, а о том, что (1) в бухгалтерии он недопустим, и (2) защита в .NET Decimal от него есть только в виде «а вы не допускайте таких больших чисел».
double fd1 = 0.33d;
double fd2 = Math.Round(0.33d, 2);
Console.WriteLine("{0:F30} {1:E30}", fd1, fd1);
Console.WriteLine("{0:F30} {1:E30}", fd2, fd2);
fd2 *= 10d;
Console.WriteLine("{0:F30} {1:E30}", fd2, fd2);
fd2 -= 3d;
Console.WriteLine("{0:F30} {1:E30}", fd2, fd2);
fd2 *= 10d;
Console.WriteLine("{0:F30} {1:E30}", fd2, fd2);
fd2 -= 3d;
Console.WriteLine("{0:F30} {1:E30}", fd2, fd2);
то видно, что Math.Round ничего не изменил, а вот дальше возникает отклонение (но рантайм врёт в печати — проверено на Mono 4.6.2 и .NET Core 1.0.0-preview2-003118, результаты идентичны):
Для сравнения то же на Python3 (идеально честный вывод: он для любой заданной точности подбирает десятичную строку такую, что обратно превращается в такую же двоичную) —
он не зарезает «хвосты» там, где начинается дребезг, и видно, как именно он был заложен изначально.
> round(0.1) продемонстрировать не получилось. Я думаю, это какой-то крайний случай, когда получается точное число.
Да, и таких достаточно много. А вот на знаменитом из всяких stackoverflow примере уже лезет дребезг: тот же Python3:
> Исключение при переполнении целой части не выбрасывается, вы правы. Я спутал с decimal.
Ну да, переполнений нет, но в варианте типа
decimal d2 = 1.0e28m;
decimal d3 = 1.0e-28m;
Console.WriteLine("{0:F40}", d2 + d3);
мы получаем
следы второго слагаемого тупо потерялись :( почему я и говорю — inexact не ловится.
Можно полный пример?
> В C# я проверял — выскакивает ArithmeticOverflowException.
Аналогично. О какой операции речь?
Первый пункт я бы формулировал немного иначе, хотя по сути близко.
Идти в бухгалтерских расчётах (речь о них — не о финансовых в целом, там может быть и «высокая» математика) надо от некоторых базовых правил. В первую очередь это то, что деньги не могут появляться ниоткуда и исчезать в никуда, любая сумма и разность должны быть точными. Как бы ни были сформулированы правила для округлений в каком-то случае (НДС, проценты, другое), это особые ситуации. Лучше получить исключение переполнения, если точности не хватает, чем потерять доли (копейки) и потом искать источник несведения баланса.
Основной проблемный случай — деление на несколько частей. Например, деля 100.00 поровну на 3, мы должны получить не 33.33+33.33+33.33, а 33.33+33.33+33.34. Кому будет лишняя копейка — вопрос полиси, но она не должна потеряться неизвестно где. Аналогично, деля даже на 2: 2.33 пополам должно дать 1.16+1.17, а не 1.16+1.16, как получилось бы, если бы округляли оба результата деления по умолчанию. Проценты? Пожалуйста — 6.99 при выделении шестой части должно дать 1.16+5.83 или 1.17+5.82, но не 1.16+5.82.
Далее. Чем плоха плавучка? И двоичная, и десятичная тут плохи. Для двоичной — банальный пример: в IEEE double, 0.1*3 — 0.3 равно 5.55...e-17. Часто достаточно того, что это не 0: чтобы сравнить с нулём, надо вначале округлить до 0.01 (или какая там выбрана точность). Вообще, все расчёты, получается, надо вести так, что вместо a+b мы делаем round(round(a,0.01), round(b,0.01), 0.01) (вторым аргументом я показал единицу точности). Но так как round(a,b) обычно выполняется как b*round_int(a/b), то резонный вопрос — нафига вообще держать числа в дробном виде, если на каждую операцию они переводятся в целые, а потом обратно?
Есть десятичная плавучка — даже в IEEE754 (версии 2008 года), IBM её умеет аппаратно, многие остальные — программно (например, GCC даёт для C). Но и с ней проблемы. Decimal32 из IEEE это 7 точных десятичных цифр. Сложив 9999999 и 4, мы получим 10000000, а не 10000003, точности для последней цифры не хватит. И потеря будет молчаливой, за исключением (обычно замаскированного) inexact exception. С такими данными можно работать, но только при условии, что inexact exception всегда включен или регулярно проверяется, кроме отдельных операций, где он вреден (вдруг кому-то таки нужен квадратный корень из суммы). Более того, после операций за пределами обычного сложения/вычитания надо округлять в нужные пределы. Если 19.5% от 123.45, пусть мы даже подсчитаем проценты в float (любом), результат должен быть выдан уже как 23.46, а не 23.4555, и только этот результат имеет право быть назван суммой в выбранной валюте.
Аналогично проблемен и Decimal из C#, и даже больше: если каким-то образом вылезли за его 28 цифр, то там даже inexact exception не будет: он не умеет жаловаться на такое. Поэтому вернуться к сумме после всех вычислений надо через Round(), не доверяя любым предыдущим результатам. Но там сложно найти случай переполнения этих 28 цифр :) и это спасает на практике. Но, боюсь, не все сертификаторы положительно оценят подобный «авось» даже тогда, когда суммы в септиллионы букозоидов не бывают в принципе: контроля переполнения и потери точности нет => не годится.
Поэтому основной вариант, о котором надо думать — это масштабированные целые. Где конкретно кому — целые копейки, 1/100 цента, как-то ещё — это уже по местным требованиям.
Далее, это было только про сложение/вычитание чисел одной размерности. Но есть и другие случаи. А именно:
* одна валюта, но разные размерности (где-то копейки, где-то сотые доли копейки)
* разные валюты и деноминации (автор поста достаточно изложил, не повторяю)
* иные операции, чем сложение и вычитание однородных денег
и вот тут действительно очень на пользу иметь обёртку, которая не позволяет просто так сложить, например, деньги США в сотых долях цента с деньгами Беларуси в копейках; и конверсия каждый раз должна быть явной (с возможным указанием метода округления); никаких автоматических сложений, например, копеек с сотыми долями копейки, даже если машина знает, что валюта одинаковая.
Операции умножения на целое ещё могут проходить в том же логическом «поле» сумм, деление — уже нет (см. примеры выше), аналогично взятие процентов. Более сложная арифметика и алгебра изначально должны выполняться в пределах подходов с плавающей точкой, хотя, возможно, с расширенной точностью (single, он же float в C, очень часто недостаточен; double уже обычно годится). Возврат в финансы — только через приведение к соответствующему типу с указанием выходной точности и контролем особых входных данных типа INF или NaN.
К остальному есть немного комментариев.
> Сумма не может быть отрицательной
Даже пытался возразить по мелочам, но не смог :) на самом деле часто допускают временный уход какого-то счёта в минус; проблема возникает, когда этот минус не закрыт на какой-то момент (как конец рабочего дня). Не могу сказать, что это хорошо, но это явно не всегда плохо.
Показ переплаты как долга с минусом — да, против не готовых к такому может быть диверсионным. Не зря в некоторых традициях вместо минуса используют другую нотацию — например, [в квадратных скобках]. Хотя сейчас эти скобки, наоборот, собьют технически подготовленных.
Вот противоположный стиль — переплата с плюсом, а долг с минусом — меньше сбивает, насколько я видел; особенно если потом таки выписать «к оплате столько-то» и писать ноль в случае переплаты.
> Поэтому лучше всего использовать нейтральные понятия «сделка», «обмен», «дебет», «кредит», «зачисление», «списание» и т. д.
Это ещё менее понятно, just IMHO. Подозреваю, хоть как-то универсально понятным будут варианты типа «сторона A получает...», «сторона B получает...»
Вопрос: а зачем? Вычисления в целых числах заведомо не имеют подобной проблемы и обходятся обычно дешевле (сумма целых даже в виде Decimal из C# проще, чем два round плюс суммирование и масштабирование).
> Как вариант: хранить суммы в полях int в значениях наименьшой возможной единицы (копейки, центы) и делить их на степень кратности к денежной единице только перед выводом на экран/печать. Кстати, такие реализации встречал неоднократно.
Именно. Фактически, для целей финансов это самый нормальный режим. (С поправкой: не только печать, но и всякая конверсия; иногда и в какой-нибудь float надо переводить — например, проценты взять.)
> Покупка/продажа валюты, акций и т.д. во всем мире идет как bid/ask. Оператор рынка (банк в данном случае) объявялет обменные курсы, по которым покупает и продает. Если юзер не понимает таких элементарных вещей, то лучше ему не заниматься такими операциями.
Практически любой человек, который ездит за границу, вынужден думать о покупке/продаже валюты. Предлагаете вообще не ездить?
Случай векторизации я тут не рассматривал. Вообще у нас дискуссия как-то странно идёт — одни и те же аргументы применяются несколькими сторонами то в контексте конкретного примера, то в контексте высокоскоростного кода, который надо писать, чтобы компилятор его оптимизировал и векторизовал… половина путаницы из-за этого.
> используются на несколько порядков чаще, чем вещи типа кодеков
Ну вот моя специфика тут пары последних лет это где-то «кодеки». Параллельности немного (ещё и навязанные средства мешают), а вот путаницы в форматах разборе данных ой-ой. Пока что по результатам этой дискуссии пришлось добавить -fno-strict-aliasing на пару исходников, причём я подозреваю, что если я туда повлепливаю memcpy, то меня не поймут кое-какие коллеги.
> GPU — это хороший пример. Ни один GPU не даёт вам возможности «штатно» программировать его на ассемблере.
И снова — я имел в виду вообще переписывать код, а Вы вдруг ассемблер вспомнили :) почему?
> Кармак отлично знает ассемблер
> однако в данном конкретном случае он-таки предпочёл остаться в рамках C.
А вот тут уже очень интересный момент. Если он «предпочёл остаться в рамках C» (за это Вы его хвалите), но применил вместо разрешённого подхода — трюк, про который говорите «никогда-никогда!» (и который не был допустим даже по C89, насколько я нагуглил), о чём это говорит? Он был прав или нет?
Авторы Quake, я предполагаю, вообще не озадачивались подобным выбором, зная свою целевую обстановку. Агрессивные оптимизации, которые могли бы испортить такой код, тогда не применялись ни одним компилятором, доступным для Windows; тогда только-только начинала появляться теория таких оптимизаций. Например, книга Мучника — одно из классических изданий такого рода — это 1997-й, но реальное появление чего-то такого в доступных компиляторах это уже 2000-е. Формализация проблемы это тоже стандарт C99, не раньше(?)
Зная, что тут проблемы нет, авторы Quake могли применять подобные фишки в полный рост, не опасаясь последствий.
Кроме того, сам факт бинарной поставки мог позволить доточить по вкусу, где нужно, даже после компилятора :)
А вот для современного применения, с этим образцовым кодом уже проблемы. И если то, что коллега Halt взвился на этот код по нынешним меркам, было просто реакцией на персональную разновидность красной тряпки (у кого что болит...), и за пределами основной темы статьи, то Ваше предложение проверить на юнит-тестах, увы, совсем некорректно.
На movd с аналогом? Это что же должно случиться, чтобы тут возникло замедление в 10 раз?
> вы считаете что на каждый критичный участок нужно посадить программиста, который будет следить за изменениями в мире процессоров и постоянно подкручивать ваш код?
Я не «считаю», я вижу, что оно сейчас именно так и происходит. Каждый год от смены чего-то «утекают» как минимум несколько процентов от результата. Иногда — в разы (как с приходом GPU).
Почему она важна? Потому что, по тому же процитированному автором статьи:
> свобода номер 0: возможность запускать программу с любой целью
для СУБД (как и любого сетевого сервера) это означает, что она может использоваться клиентскими программами любой лицензии — аналогично тому, как программа под GPL может использоваться под управлением сколь угодно проприетарного комплекта софта, взаимодействуя с последним через штатные каналы связи, через файлы и т.д.
Но СУБД имеют свой протокол. Для MySQL лицензия утверждает совместимость GPL-версии штатного клиента только с явным и закрытым (нерасширяемым) списком FOSS-лицензий. (Список, кстати, странный — в нём нет даже GPLv2.) Коммерческий продукт обязан брать коммерческую лицензию или использовать отдельный процесс-переходник (по сути, последний вариант — хак для лицензии). (Разумеется, есть альтернативные клиентские библиотеки. Но это таки другой продукт.)
Этого для меня, как и, думаю, для большинства потенциальных разработчиков программ — клиентов БД, достаточно, чтобы считать MySQL несвободным: его свобода ограничивает чужую свободу в тех местах, где штатно предполагается отсутствие ограничительных рамок на чужие лицензии.
И вот тут PQ отличается — лицензия клиента по сути тождественна BSD или MIT и не ограничивает лицензирование того, кто использует штатную реализацию клиента.
Не хочу тут раздувать новый спор про уже известные проблемы, потому что они уже обсуждены, кажется, чуть менее чем всеми, за последние 10-15 лет: это темы GPLv3 vs. GPLv2, тема свободы разработчиков против свободы пользователей и частный случай GPL vs. BSD в вопросах построения не-FOSS продуктов на базе FOSS. Ищущий да обрящет. Мой посыл был в том, что статья подобного рода, как тут, без упоминания той тематики, что я назвал — бессмысленна и вводит в заблуждение. Спасибо за внимание.