Комментарии 84
У меня остаётся надежда, что удобный алиас uint128_t попадёт в стандарт — тогда я смогу выкинуть свой тип UInt128, пока его не увидел кто-то ещё. =)
А что вам мешает использовать предложенный к стандартизации шаблон прямо сейчас — вместо класса UInt128? Заодно обкатаете свое предложение в реальном проекте? ;-)
2. Но даже если ваше изменение примут, оно не решает же поставленную задачу, потому что в Microsoft Visual Studio 2015 изменение никогда не появится.
Задача итак решена, просто не эстетично. Разговор про то, чтобы не писать велосипеды в дальнейшем.
2. В этой версии MSVS останется велосипед, в будущих появятся изменения, всем хорошо, все довольны.
Не совсем. Негативные эффекты есть — стандарт C++ станет жирнее. А если эта фича будет добавлена в какой-нибудь популярный хедер, то ВСЕ его использующие получат пенальти на скорость компиляции, даже если им фича не нужна. Хотя фича прекрасно могла быть реализована в виде подключаемой библиотеки, без залезания в стандарт.
2. Появится в более поздникх MSVC, а в более ранних можно скомпилировать библиотеку.
Тут не proposal нужен, а своевременная синхронизация с последним доступным стандартом C на момент принятия нового стандарта C++
Например, restrict отсутствует в С++Как будто это хорошо, restrict как минимум позволяет компилятору проводить более сильные оптимизации, т.к есть более сильные ограничения на область памяти. Из фич C99, которых нет в стандарте меня лично бесит designated initializers, и нет initializer list не замена, т.к надо помнить порядок полей структуры, жаль что только clang поддерживает их в std=c++11
restrict
поддержат…Однако возможное ускорение заметно.
возможное. Ситуаций, где restrict имеет значение, очень мало. Тем более, что c++ по стандарту считает, что указатели на разные типы не алиасятся (кроме void*/char*)
Так что фича то нужная… хоть и не так широко.
Ну например с ним openCL компилятор, что от amd, что от altera позволяет ускорить код где-то на 5-7%.
В частности, комиссия всё-таки попросила в wide_int оперировать количеством машинных слов
Стандарт же вроде не определяет размер машинного слова. Как быть если надо именно int2048_t?
http://www.boost.org/doc/libs/1_64_0/libs/multiprecision/doc/html/boost_multiprecision/ref/cpp_int_ref.html
Но автор молодца — не остановился на этапе «поныть что все плохо»
Слониха и слонёнок помогают мыши прогнать кота. Это иллюстрация работы комитета С++.
Но допустим, что мы не хотим использовать ее и пишем собственную библиотеку на шаблонах wide_int. Тогда следующий вопрос к языку C: «В ассемблере уже много лет есть команда adc, которая складывает с учетом флага переноса, где она в С?»(Также еще можно поставить вопрос про SIMD и конвейерные инструкции). И количество таких вопросов огромно, когда мы начинаем копаться в возможностях С и ассемблера… И что самое важное, это полезные фичи ускоряющие процесс написания и скорость исполнения кода.
В общем, как мне кажется, стоит подумать в стандарте о реализации ключевого слова Casm(ассемблер из С), который бы предоставлял возможность писать платформонезависимые вставки(возможно программы) на ассемблере.(хотя обычно к моим идеям относятся негативно)
Мне немного непонятно, что он имеет в виду под операциями… С функции? Если да, то это не совсем то что я хочу. Если мое предложение выразить в виде фрагмента кода, то получится следующее:
bool operator_plus(register dx, register si){
unsigned char flag;
Casm{
lodsl //перенести из указателя esi данные в eax
adcl ax, [dx] //сложение с учетом флага переноса
addl dx, 4 //инкремент второго указателя(первый - автоматически)
stosl //положить в edi результат
lahf //загрузить регистр флагов в ah
movb flag, ax //перебросить в переменную flag
}
return flag & 1; //если в результате суммирования возникло переполнение типа возвращаем его
}
Очень похожим способом реализовывались ассемблерные вставки в TurboС30...(1992 год выпуска)(в качестве компилятора asm тогда использовался TASM). У меня даже был опыт написания драйвера мыши под него, откуда и возник мой никнейм.
Синтаксис ассемблера можно выбрать из существующих или придумать свой. Но он не должен вставляться напрямую в ассемблер, а проходить процесс трансляции в компиляторе С.
в Сasm-е написал не совсем правильный код, но общая идея понятна.Не совсем.
Синтаксис ассемблера можно выбрать из существующих или придумать свой. Но он не должен вставляться напрямую в ассемблер, а проходить процесс трансляции в компиляторе С.Ну если он всё равно будет проходить через «процесс трансляции», то чем вам интринзики не угодили?
По моему вы сейчас медленно и со скрипом изобретаете GCC'шные built'ины — всякие __builtin_clz и __builtin_ctz, __builtin_popcount и __builtin_parity, __builtin_add_overflow и __builtin_sub_overflow — они как раз спроектированы так, чтобы ложиться в одну инструкцию в процессорах где они есть и эмулироваться там, где их нет…
А предложение выше как раз и заключается в том, чтобы выбрать некоторый набор этих интринзиков и включить в стадарт… правда оно обрывается на самом интересном месте: нет списка интринзиков, процессоров и реализуемости — а это, как бы, самое сложное, могут сотни часов уйти на штудирование мануалов, чтобы приличный набор изобразить…
Для примера: на ARM нет инструкции tzcnt, так что вместо неё используется rbit (разворот всего 32-битного регистра на 180 градусов) и lzcnt. А теперь представьте что у вас такое — где-нибудь во внутреннем цикле… хорошо будет только производителям кулеров. Ну ещё продавцы электроэнергии порадуются…
Он гарантирует правильное исполнение, но не гарантирует скорости.Но если ваш Casm «гарантирует правильное исполнение, но не гарантирует скорости», то нафиг он вообще нужен?!
Об этих интструкциях, ксати, и писал человек в предложении к стандарту.Угу — только без статистики. Так как эти интринцики уже есть, то разумное предложение включало бы в себя список интринзиков, которые используются какими-нибудь распространённые библиотеками, далее — разбивка по процессорам (тут есть, тут нет, тут можно табличку приспособить).
Куча достаточно муторной работы. А сказать «сделайте мне хорошо» — это не предложение, а так, словоблудие…
По сути меня вполне устроят интринзики, если компилятор умеет их превращать в одну инструкцию на поддерживающих платформах и в несколько на не поддерживающих. Также я хочу знать где они хранятся.
Поэтому давайте закончим бессмысленную переписку… Оставьте пару ссылок на материалы по интринзикам для других интересующихся и придем к соглашению, что Casm их эквивалент в упрощенной форме…
Я их также изучу(код реализации) и посмотрю, какие у меня к ним появятся замечания
По сути меня вполне устроят интринзики, если компилятор умеет их превращать в одну инструкцию на поддерживающих платформах и в несколько на не поддерживающих.Примерно так gcc'шные интринзики и устроены. А вот Intel'овские и ARM'овские — не так: там если процессор инструкцию не поддерживает — то и интринзик вызвать нельзя.
Также я хочу знать где они хранятся.Что значит «где хранятся»? В исходниках компилятора и хранятся. К примеру
__builtin_popcount
. Вот тут в LLVM, здесь — в GCC. Я тут вот — табличка для старых процессоров.Оставьте пару ссылок на материалы по интринзикам для других интересующихся и придем к соглашению, что Casm их эквивалент в упрощенной форме…Casm — это их эквивалент в усложнённой форме. В случае с интринзиками — никто, кроме компилятора, не знает, что это не функции, никаких расширений в язык добавлять не нужно и ничего нигде особо обрабатывать не нужно. Если очень приспичит — можно реализовать их в виде просто библиотеки (пример — NEON'овские интринзики для x86). А что такое ваш Casm, и как его, в принципе, использовать — я думаю вы и сами не понимаете…
А материалы… Википедия не устроит? Не думаю, что есть что-то более структурированное.
SIMD в удобных случаях компиляторы уже научились использовать.
Я о приросте конкретной вашей операции — сложение с переносом — прежде чем предлагать, нужно оценить выгоду, и в каких случаях она достигается.
Иначе это не предложение к Стандарту а просто ППР.
Я о приросте конкретной вашей операции — сложение с переносом — прежде чем предлагать, нужно оценить выгоду, и в каких случаях она достигается.Сложение с переносом ещё так себе (можно выкручиваться по-разному и компиляторы это уже умеют прилично распознавать и оптимизировать).
А вот умножение с детектированием переполнения — это жуть: на большинстве процессоров это делается в пару команд, можно также использовать __int128 и clang/gcc сгенерят приличный код, но в «чистом» C/C++ этого никак не сделать! А количество ошибок, которые порождаются из-за этого — на миллиарды долларов, я думаю. Тут вопрос даже не просто в скорости. Просто если проверка дешевая — её будут вызывать, если дорогая — будут пытаться обойтись без неё.
Что компиляторы умеют распознавать и оптимизировать? Вот примерно такой код:
s.low = a.low + b.low;
s.hi = a.hi + b.hi + (s.low < a.low); //carry
никак не хочет превращаться в adc, компиляторы упорно выдают сравнение и условный переход.
Вот как раз умножение 64bit64bit=>128bit можно при отсутствии родного int128 записать как четыре умножения 32bit32bit=>64bit и несколько сложений, и при этом без ручного контроля переноса. Как-то так:
void xmul128(uint64_t &hi, uint64_t &lo, uint64_t a, uint64_t b)
{
uint64_t x0,x1,x2,x3;
const uint32_t al = (uint32_t)(a);
const uint32_t ah = (uint32_t)(a >> 32);
const uint32_t bl = (uint32_t)(b);
const uint32_t bh = (uint32_t)(b >> 32);
x0 = (uint64_t)(ah) * bh; //high
x1 = (uint64_t)(al) * bh; //mid1
x2 = (uint64_t)(ah) * bl; //mid2
x3 = (uint64_t)(al) * bl; //low
x2 += x3 >> 32; // no carry: max (2^32-1)^2 + 2^32-1
x0 += x2 >> 32;
x1 += (uint32_t)(x2); // still no carry
hi = x0 + (x1 >> 32);
lo = (x1 << 32) | (uint32_t)(x3);
}
Вот примерно такой код:Это смотря какие компиляторы.
никак не хочет превращаться в adc, компиляторы упорно выдают сравнение и условный переход.s.low = a.low + b.low; s.hi = a.hi + b.hi + (s.low < a.low); //carry
#include <inttypes.h>
struct pair {
uint64_t low;
uint64_t hi;
};
pair add(pair& a, pair& b) {
pair s;
s.low = a.low + b.low;
s.hi = a.hi + b.hi + (s.low < a.low); //carry
return s;
}
Сходите на godbolt и сравните. Вот что выдаёт clang:
add(pair&, pair&): # @add(pair&, pair&) movq (%rsi), %rax movq 8(%rsi), %rdx addq 8(%rdi), %rdx addq (%rdi), %rax adcq $0, %rdx retq
Вот что выдаёт gcc 4.4:
add(pair&, pair&): movq (%rdi), %rax movq 8(%rsi), %rcx addq 8(%rdi), %rcx addq (%rsi), %rax setb %dl movzbl %dl, %edx leaq (%rcx,%rdx), %rdx retКонечно «movzbl %dl, %edx» доставляет и setb вместо adc — не очень… но переходом нет!
Последние версии GCC, правда, окосели и действительно генерируют бог знает что — ну так если багрепортов нет, то никто это и не починит.
Вот как раз умножение 64bit64bit=>128bit можно при отсутствии родного int128 записать как четыре умножения 32bit32bit=>64bit и несколько сложений, и при этом без ручного контроля переноса.Можно. Но скорость у этого будет…
Но я о другом. Вам приходит (снаружи) количество элементов и их размер. И вам нужно элементарно оценить размер буфера под это дело. Как? Простой контроль входных данных — а эффективно это реализовать невозможно. Бред ведь!
Последние версии GCC, правда, окосели и действительно генерируют бог знает что
Поигрался — gcc 6.3 при -O2 выдаёт непонятно что, а при -O1:
add(pair&, pair&):
mov rax, QWORD PTR [rdi]
mov rdx, QWORD PTR [rsi+8]
add rax, QWORD PTR [rsi]
adc rdx, QWORD PTR [rdi+8]
ret
В msvc есть _umul128(), в gcc/clang __int128. Но под 32 битные платформы всего этого нет. И там все равно надо 4 умножения, быстрее не получится.
И там все равно надо 4 умножения, быстрее не получится.Не получится… что, я извиняюсь? Перемножить два 64-битных числа? Для получения младшей части, в общем-то, достаточно трёх. А для проверки на переполнение при перемножении двух 32-битных часел достаточно и одного:
#include <inttypes.h>
bool multiply_with_overflow(uint32_t x, uint32_t y, uint32_t& result) {
uint64_t extended_result = uint64_t(x) * uint64_t(y);
result = extended_result;
if (uint32_t(extended_result) != extended_result)
return false;
return true;
}
.file "test.cc"
.text
.p2align 4,,15
.globl _Z22multiply_with_overflowjjRj
.type _Z22multiply_with_overflowjjRj, @function
_Z22multiply_with_overflowjjRj:
.LFB4:
.cfi_startproc
movl 8(%esp), %eax
mull 4(%esp)
movl 12(%esp), %ecx
testl %edx, %edx
movl %eax, (%ecx)
sete %al
ret
.cfi_endproc
.LFE4:
.size _Z22multiply_with_overflowjjRj, .-_Z22multiply_with_overflowjjRj
.ident "GCC: (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4"
.section .note.GNU-stack,"",@progbits
Однако, во-первых. без типа
__int128
это не расширяется на 64-битные платформы, а во-вторых — код с __builtin_mul_overflow
всё равно чуть-чуть эффективнее (особенно для чисел со знаком, хотя, стоит признать, на практике это чуть реже встречается).А уж при переносе на другую платформу… Практика показывает, что перенести ассемблерный код с одной платформы на другую — зачастую очень и очень дорого. Вот в вашем «псевдоассемблере»' флаги возвращаются. И что — будем считать PARITY на ARM'е? Это сожрёт весь выигрыш от эффективного «сложения с переносом»!
И не забудьте о том, что CARRY флаг одинаков на x86 и arm'е при сложении, но отличается при вычитании! А это, на минуточку, два самых распространённых на сегодня класса процессоров (причём arm более популярен).
Но тут мы, как бы, обсуждаем предложения для комитета по стандартизации. А у них всё просто: C (и C++) — языки, предназначенные для написания переносимых программ и, соответственно, ничего «для процессора определенной заранее архитектуры» в них быть не может.
Что касается «платформонезависимых ассемблерных вставок», хотелось посмотреть, как вы себе это представляете.
Платформонезависимость ассемблера, как я уже написал выше, достигается процессом трансляции(построчный перевод нашего кода в код ассемблера) на этапе компиляции программы.
Можно очень долго перечислять разницу только между этими тремя архитектурами. А ещё есть mips, openrisc, sh, ia64, avr32, arc и т.д. А так же всякая экзотика типа DSP или машин с аппаратным стеком.
В результате надо или разрешить в casm только подмножество общих инструкций или разрешить транслировать одну инструкцию в несколько. Но тогда можно легко вылететь за ограничение относительного JMP, например. И в большинстве случаем код транслированный из такого ассемблера будет больше и медленнее, чем код сгенерированный компилятором под целевую архитектуру.
А можно ссылку на референс, где amd64(em64t) позволяет mov память память?
В меру свободного времени потестировал, занёс найденное в issues. Но это было очень поверхностное тестирование, далеко от того которое требуется для числовой библиотеки.
Нетрудно догадаться, что фокус внимания не был нацелен на полностью корректное поведение
Приоритеты примерно такие:
0) полный набор методов для реализации интерфейса
1) POD
2) constexpr
3) noexcept
4) common_type
5) корректность поведения
6) читаемость
Такой низкий приоритет продиктован тем, что в конечном счёте в std из этой имплементации не попадёт ничего. Главное, что примерно работающий код с заявленным интерфейсом можно написать.
Еще раз спасибо за отклик
Я тут пару лет назад (огого) 7 лет назад делал похожую штуку:
https://github.com/k06a/boolib/blob/master/boolib/util/Intx2.h
Насколько я помню только с делением возникли серьезные проблемы.
Я недавно начал заново: https://github.com/k06a/IntInt
Скажите, пожалуйста, есть ли какой-то прогресс по принятию этого предложения? Не смог найти никаких упоминаний после https://isocpp.org/files/papers/n4739.pdf в 2017 "discussed but not approved"
Моё субъективное впечатление: это какой-то политический процесс внутри комитета.
Может быть, antoshkka сможет чуть больше рассказать про работу в направлении больших чисел.
Как я писал предложение к стандарту С++