Допустим, у нас есть массив чисел с плавающей запятой, и мы хотим их суммировать. Можно наивно подумать, что их достаточно просто сложить, например, на Rust:

fn naive_sum(arr: &[f32]) -> f32 {
    let mut out = 0.0;
    for x in arr {
        out += *x;
    }
    out
}

Однако это запросто может привести к произвольно большой накопленной погрешности. Давайте проверим:

naive_sum(&vec![1.0;     1_000_000]) =  1000000.0
naive_sum(&vec![1.0;    10_000_000]) = 10000000.0
naive_sum(&vec![1.0;   100_000_000]) = 16777216.0
naive_sum(&vec![1.0; 1_000_000_000]) = 16777216.0

Ой-ёй… Что произошло? Проблема в том .что следующее 32-битное число с плавающей запятой после 16777216 — это 16777218. Так что при вычислении 16777216 + 1, значение округляется до ближайшего числа с плавающей запятой, имеющей чётную мантиссу, то есть снова до 16777216. Мы зашли в тупик.

К счастью, есть более совершенные способы суммирования массива.

Попарное суммирование

Чуть более умный способ — это попарное суммирование. Вместо абсолютно линейной суммы с единственным накопителем он рекурсивно суммирует массив, разбивая его пополам, суммируя половины, а затем складывая суммы.

fn pairwise_sum(arr: &[f32]) -> f32 {
    if arr.len() == 0 { return 0.0; }
    if arr.len() == 1 { return arr[0]; }
    let (first, second) = arr.split_at(arr.len() / 2);
    pairwise_sum(first) + pairwise_sum(second)
}

Этот способ более точен:

pairwise_sum(&vec![1.0;     1_000_000]) =    1000000.0
pairwise_sum(&vec![1.0;    10_000_000]) =   10000000.0
pairwise_sum(&vec![1.0;   100_000_000]) =  100000000.0
pairwise_sum(&vec![1.0; 1_000_000_000]) = 1000000000.0

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

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

fn block_pairwise_sum(arr: &[f32]) -> f32 {
    if arr.len() > 256 {
        let split = (arr.len() / 2).next_multiple_of(256);
        let (first, second) = arr.split_at(split);
        block_pairwise_sum(first) + block_pairwise_sum(second)
    } else {
        naive_sum(arr)
    }
}

Суммирование Кэхэна

Наихудший случай погрешности округления наивного сумирования масштабируется при суммировании ? элементов как ?(??), где ? — это машинный ноль вашего типа с плавающей запятой (здесь это 2−24). Показатели попарного суммирования улучшают этот результат до ?((log⁡?)?+??2). Суммирование Кэхэна ещё больше улучшает показатель (до ?(??2)), полностью избавляя нас от члена ? и оставляя только ?2 , который несущественен, если только вы не суммируете очень большое количество чисел.

Все эти границы масштабируются как ∑?∣??∣, то есть для суммирования Кэхена граница абсолютной погрешности в наихудшем случае всё равно остаётся квадратичной относительно ?.

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

pub fn kahan_sum(arr: &[f32]) -> f32 {
    let mut sum = 0.0;
    let mut c = 0.0;
    for x in arr {
        let y = *x - c;
        let t = sum + y;
        c = (t - sum) - y;
        sum = t;
    }
    sum
}

Суммирование Кэхэна заключается в хранении суммы в двух регистрах: настоящей общей суммы и небольшого значения коррекции погрешности ?. Если бы мы использовали бесконечно точную арифметику, то ? всегда бы был нулём, но при плавающей запятой такого быть не может. Недостаток такого подхода заключается в том, что для добавления каждого числа к сумме теперь требуется не одна, а целых четыре операции.

Чтобы решить эту проблему, мы можем сделать нечто подобное тому, что проделали с попарным суммированием. Сначала можно накапливать блоки в суммы наивным образом, а потом комбинировать суммы блоков при помощи суммирования Кэхэна, чтобы снизить лишнюю трату ресурсов ценой точности:

pub fn block_kahan_sum(arr: &[f32]) -> f32 {
    let mut sum = 0.0;
    let mut c = 0.0;
    for chunk in arr.chunks(256) {
        let x = naive_sum(chunk);
        let y = x - c;
        let t = sum + y;
        c = (t - sum) - y;
        sum = t;
    }
    sum
}

Точное суммирование

Я знаю по крайней мере два обобщённых способа получения корректно округлённой суммы последовательности чисел с плавающей запятой. Они логически вычисляют сумму с бесконечной точностью, а в конце округляют её до значения с плавающей запятой.

Первый способ основан на примитиве 2Sum, то есть на избавленном от погрешностей преобразовании из двух чисел ?,?  в ?,? так, что ?+?=?+?, где ? — это малая погрешность. Многократно применяя это преобразование, пока погрешность не пропадёт, мы можем получить корректно округлённую сумму. Сложность заключается в отслеживании того, что и в каком порядке складывать, а для исчезновения всех членов в наихудшем случае требуется ?(?2) сложений. Именно это реализовано в math.fsum языка Python и в крейте Rust fsum, которые используют дополнительную память для хранения частичных сумм. Крейт accurate тоже реализует это при помощи преобразования на месте в i_fast_sum_in_place.

Другой способ заключается в хранении большого буфера целых чисел, по одному на экспоненту. В случае прибавления числа с плавающей запятой оно раскладывается на экспоненту и мантиссу, а мантисса прибавляется к соответствующему целому числу в буфере. Если происходит переполнение целого числа buf[i], то выполняется инкремент целого числа в buf[i + w], где w — ширина целого числа.

На самом деле, этот способ позволяет вычислять абсолютно точную сумму без какого-либо округления; по сути, это более свободное представление числа с фиксированной запятой, оптимизированное для накопления чисел с плавающей запятой. Этот способ требует времени ?(?), но использует большой, хотя и константный объём памяти (≈≈ 1 КБ в случае f32, ≈≈ 16 КБ в случае f64). Преимущество этого способа ещё и в том, что это онлайновый алгоритм — и прибавление числа к сумме, и получение текущего общего значения занимают амортизированное ?(1).

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

Раскрываем мощь компилятора

Кроме точности, у naive_sum есть ещё одна проблема. Компилятор Rust не может изменять порядок сложения чисел с плавающей запятой, потому что такое сложение не ассоциативно. Поэтому он не может автоматически векторизировать naive_sum, чтобы та использовала для вычисления суммы команды SIMD, и не может применять параллелизм на уровне команд.

Для решения этой проблемы в компиляторе Rust есть встроенные средства (intrinsics), выполняющие суммирование чисел с плавающей запятой, в то же время обеспечивая ассоциативность, например, std::intrinsics::fadd_fast. Однако такие команды невероятно опасны, потому что они предполагают, что и входные, и выходные значения являются конечными числами (не бесконечностями и не NaN), в противном случае они оказываются неопределённым поведением. Функционально это делает их неприменимыми, поскольку только в самых ограниченных ситуациях при вычислениях суммы мы знаем, что все входные числа являются конечными и что их сумма не может привести к переполнению.

Недавно я поделился своим разочарованием этими операторами с Беном Кимоком, и вместе мы предложили (а он реализовал) новое множество операторов: std::intrinsics::fadd_algebraic и ему подобные.

Я предложил, чтобы мы назвали эти операторы алгебраическими (algebraic), потому что они позволяют (теоретически) выполнять любые преобразования, допустимые в вещественной алгебре. Например, подстановки ?−?→0, ??+??→?(?+?) или ?6→(?2)3. В общем случае эти операторы обрабатываются, как будто они выполняются с использованием вещественных чисел, и их можно отобразить на любое множество команд с плавающей запятой, которые будут эквивалентом исходного выражения при условии, что команды с плавающей запятой будут точными.

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

Кроме того, они позволяют генерировать команды fused multiply-add , так как в вещественной алгебре fma⁡(?,?,?)=??+?.

Благодаря этим новым командам можно очень легко сгенерировать автоматически векторизированную сумму:

#![allow(internal_features)]
#![feature(core_intrinsics)]
use std::intrinsics::fadd_algebraic;

fn naive_sum_autovec(arr: &[f32]) -> f32 {
    let mut out = 0.0;
    for x in arr {
        out = fadd_algebraic(out, *x);
    }
    out
}

Если выполнить компиляцию с -C target-cpu=broadwell , то мы увидим, что компилятор автоматически сгенерировал следующий короткий цикл с четырьмя накопителями и командами AVX2:

.LBB0_5:
    vaddps  ymm0, ymm0, ymmword ptr [rdi + 4*r8]
    vaddps  ymm1, ymm1, ymmword ptr [rdi + 4*r8 + 32]
    vaddps  ymm2, ymm2, ymmword ptr [rdi + 4*r8 + 64]
    vaddps  ymm3, ymm3, ymmword ptr [rdi + 4*r8 + 96]
    add     r8, 32
    cmp     rdx, r8
    jne     .LBB0_5

Он обрабатывает 128 байтов данных с плавающей запятой (то есть 32 элемента) за 7 команд. Кроме того, все команды vaddps не зависят друг от друга, потому что накопление выполняется в разные регистры. Если проанализировать код при помощи uiCA, то можно увидеть. что по его оценке выполнение этого цикла занимает 4 такта, то есть обрабатывается по 32 байта на цикл. При частоте 4 ГГц это до 128 ГБ/с! Стоит отметить, что это гораздо больше, чем пропускная способность ОЗУ моей машины, так что этой скорости можно достичь, только суммируя данные, уже находящиеся в кэше.

Помня об этом, мы также может легко определить block_pairwise_sum_autovec и block_kahan_sum_autovec, заменив их вызовы к naive_sum  на naive_sum_autovec.

Точность и скорость

Давайте попробуем сравнить разные способы суммирования. Попробуем в качестве бенчмарка просуммировать 100000 случайных float в интервале от -100000 до +100,000. Это 400 КБ данных, так что всё равно уместится в кэш моего AMD Threadripper 2950x.

Весь код выложен на Github. Компиляция выполнена с RUSTFLAGS=-C target-cpu=native и --release. Я получил следующие результаты:

Алгоритм

Производительность

Средняя абсолютная погрешность

naive

5.5 ГБ/с

71.796

pairwise

0.9 ГБ/с

1.5528

kahan

1.4 ГБ/с

0.2229

block_pairwise

5.8 ГБ/с

3.8597

block_kahan

5.9 ГБ/с

4.2184

naive_autovec

118.6 ГБ/с

14.538

block_pairwise_autovec

71.7 ГБ/с

1.6132

block_kahan_autovec

98.0 ГБ/с

1.2306

crate_accurate_buffer

1.1 ГБ/с

0.0015

crate_accurate_inplace

1.9 ГБ/с

0.0015

crate_fsum

1.2 ГБ/с

0.0000

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

Во-первых, хотелось бы отметить, что разница производительности между самым быстрым и самым медленным способом составляет более чем 100 раз. И это для суммирования массива! Это может быть не совсем честно, потому что самые медленные способы вычисляют нечто существенно более сложное, но всё равно существует разница в двадцать раз между достаточно разумной наивной реализацией и самым быстрым способом.

Мы обнаружили, что в общем случае способы _autovec, использующие fadd_algebraic, быстрее и точнее, чем те, которые используют обычное сложение с плавающей запятой. Они точнее по той же причине, по которой точнее попарная сумма: любое изменение порядка сложений лучше, поскольку используемая по умолчанию длинная цепочка сложений уже является наихудшим случаем точности точности в сумме.

Ограничившись Парето-оптимальными вариантами, мы получим следующие четыре реализации:

Алгоритм

Производительность

Средняя абсолютная погрешность

naive_autovec

118.6 ГБ/с

14.538

block_kahan_autovec

98.0 ГБ/с

1.2306

crate_accurate_inplace

1.9 ГБ/с

0.0015

crate_fsum

1.2 ГБ/с

0.0000

Отметим, что разницы в реализации могут влиять достаточно сильно; к тому же, вероятно, существуют ещё десятки способов компенсированного суммирования, которые я не сравнивал.

Думаю, в большинстве случаев здесь выигрывает block_kahan_autovec, он имеет хорошую точность (не происходит вырождение при больших входных значениях) и почти максимальную скорость. В большинстве сценариев использования дополнительная точность, получаемая благодаря корректному округлению сумм, не требуется, а она в 50-100 раз медленнее.

Разбив цикл на явный остаток плюс короткий цикл сумм по 256 элементов, мы можем ещё немного повысить производительность и избежать пары операций с плавающей запятой для последнего блока:

#![allow(internal_features)]
#![feature(core_intrinsics)]
use std::intrinsics::fadd_algebraic;

fn sum_block(arr: &[f32]) -> f32 {
    arr.iter().fold(0.0, |x, y| fadd_algebraic(x, *y))
}

pub fn sum_orlp(arr: &[f32]) -> f32 {
    let mut chunks = arr.chunks_exact(256);
    let mut sum = 0.0;
    let mut c = 0.0;
    for chunk in &mut chunks {
        let y = sum_block(chunk) - c;
        let t = sum + y;
        c = (t - sum) - y;
        sum = t;
    }
    sum + (sum_block(chunks.remainder()) - c)
}

Алгоритм

Производительность

Средняя абсолютная погрешность

sum_orlp

112.2 ГБ/с

1.2306

Разумеется, можно попробовать менять 256 на другие значения; я выяснил, что при 128 производительность снижается примерно на 20%, а при 512 производительность особо не повышается, но страдает точность.

Заключение

Я считаю, что fadd_algebraic и схожие алгебраические intrinsics очень полезны в достижении высокоскоростных процедур работы с плавающей запятой, и что их нужно добавить и в другие языки. Глобальный -ffast-math недостаточно хорош, так как мы уже видели, что наилучшая реализация оказалась гибридом между автоматически оптимизированными для скорости вычислениями и вручную реализованными неассоциативными компенсированными операциями.

Кроме того, при использовании LLVM следует опасаться -ffast-math. Когда этот флаг установлен в LLVM, получение NaN или бесконечности становится неопределённым поведением. Понятия не имею, почему разработчики выбрали такую жёсткую позицию, делающую ненадёжной практически все использующие его программы. Если вы используете в качестве целевой платформы для своего языка LLVM, то избегайте флагов fast-math nnan и ninf.

Комментарии (38)


  1. vasan
    04.06.2024 12:15
    +1

    Если нужна точность, то зачем работать с числами типа float?

    Для этого есть double и long double.)


    1. unreal_undead2
      04.06.2024 12:15

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

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

      На мой взгляд это скорее common case.

      Если вы используете в качестве целевой платформы для своего языка LLVM, то избегайте флагов fast-math nnan и ninf

      При расчёте параметров ядерного реактора или ракетного двигателя - однозначно. В игрушках и подобном софте можно и пожертвовать корректной обраткой nan/inf.


      1. vasan
        04.06.2024 12:15

        с double проблем меньше )

        Кстати если точность является критичной, то имеется библиотеки для вычислений с плавающей запятой многократной точности, например, GNU MPFR Library.


        1. mentin
          04.06.2024 12:15

          Я думаю float это в статье для простоты демонстрации. На практике конечно [long] double, но их тоже не хватает. MPFR будет практически всегда медленнее, чем алгоритмы для конкретного применения, вроде алгоритма Кэхэна для суммы.


          1. vasan
            04.06.2024 12:15

            Всё зависит от конкретной цели, где это применяется. Где нужно быстродействие, там проигрывает точность, и наоборот. Пока широко не внедрят специализированные АЛУ для работы с числами произвольной точности, говорить об одновременном быстродействии и точности машинной арифметики считаю не имеет смысла.


            1. mentin
              04.06.2024 12:15
              +2

              Вот статья как раз показывает, что такой подход для конкретного (но очень популярного) применения - не правилен, АЛУ не нужно, числа произвольной точности в данных применениях не нужны. Есть алгоритмы, позволяющие рассчитать максимальную погрешность, и выбрать самый быстрый алгоритм обеспечивающий нужную вам точность. И этот алгоритм будет гораздо быстрее, чем код "тупо" использующий MPFR или другие библиотеки чисел с расширенной точностью (которыми тоже надо правильно пользоваться, чтобы правильно рассчитать точность чисел, дающую нужную точность результата).


              1. vasan
                04.06.2024 12:15

                В принципе соглашусь с вами насчёт выбора алгоритма Кэхэна для суммы. Однако всё же следует ориентироваться с выбором алгоритма в зависимости от предъявляемых требований к задаче, так как возможен вариант, что стандартные типы чисел с плавающей запятой изначально не смогут удовлетворить точность вычисления.


                1. MiyuHogosha
                  04.06.2024 12:15

                  В ряде случаев вообще надо переходить на целые числа с ЦМР


      1. vanxant
        04.06.2024 12:15

        На мой взгляд это скорее common case.

        Угу. Если у вас во входном массиве есть хоть один NaN или бесконечность (что технически одно и то же, inf это одно из значений NaN), то, как ни суммируй, на выходе будет NaN.

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


        1. sumin_av
          04.06.2024 12:15

          И всё же с бесконечными (±inf) не всё так однозначно. В качестве слагаемого он даст неопределённость (NaN) только с inf противоположного знака, либо с NaN.

          Если же inf окажется в знаменателе, то запросто приведёт результат к обычному ±0.

          В статье, как мне кажется, очень неплохо раскрыто, что практически даром можно заложить корректное поведение кода в пограничных случаях


          1. unreal_undead2
            04.06.2024 12:15

            Отказ от -ffast-math - это ни разу не даром.


  1. vanxant
    04.06.2024 12:15
    +1

    Ещё, конечно, есть варианты с предварительной сортировкой массива. В идеале по возрастанию абсолютного значения, но можно и находить "середину" массива, отсортированного по возрастанию или убыванию, бисекцией и дальше считать в обе стороны.

    O(n log n) для сортировки это не так дорого, если например ограничиться блоками в 256 или там 4096 элементов (чтобы влезало в кэш L1). Разумеется, это нужно делать рекурсивно, т.е. на уровне выше сортировать значения сумм по 256 элементов. Точность при этом будет на уровне accurate алгоритмов, а скорость - на уровне блочных.


    1. speshuric
      04.06.2024 12:15

      А есть замер?


  1. JediPhilosopher
    04.06.2024 12:15
    +2

    Работал в финтехе одно время, вот там наверное самые популярные грабли у начинающих разработчиков - это представление сумм денег в виде числа с плавающей запятой, а потом огребание вот этих самых проблем. Потому что складывать надо много, например все транзакции платежной системы за недели/месяцы - это очень много как самих операций сложения, так и потенциально большие числа сами по себе.

    Но там это обходят по-своему, используют целочисленную арифметику и ведут расчеты в самых маленьких денежных единицах, например копейках (ну или я в Нигерии работал, там были наиры и кобо). То есть хранят 20 рублей 30 копеек как целое число 2030, а не как вещественное 20.3, и никаких проблем.


    1. unreal_undead2
      04.06.2024 12:15

      В аналитике и моделировании (когда дело не касается реальных сумм на счетах) FP там всё таки уместен - скажем, классическая модель Блэка-Шоулза на float/double считается (хотя я сталкивался только с бенчмарками, а не реальным применением). А с выбором базы в целочисленных расчётах тоже надо действовать аккуратно, особенно если обрабатываются разные валюты, акции и т.д. - навскидку в официальных курсах ЦБ четыре знака после запятой.


      1. MiyuHogosha
        04.06.2024 12:15

        Практически все анализы сигналов, радарные вещи, фищхимия, навигация, управление оборудованием, игры с открытым миром (т.е. большые расстояния) - наэтом трюке с ЦМР (ценой малого разряда), точное решение ряда ДУ в свч технике и оптике. Все на накоплениях


  1. zergon321
    04.06.2024 12:15
    +1

    А как насчёт вот этого?


  1. ptr128
    04.06.2024 12:15

    Зачем воздвигать самому себе препятствия, используя там где нужна точность числа с плавающей запятой, вместо чисел с фиксированной запятой, а затем героически их преодолевать?


    1. MiyuHogosha
      04.06.2024 12:15

      Нууу... числа с фиксированной прихрдитмя придумывать самому


      1. zergon321
        04.06.2024 12:15

        Можно скачать наиболее понравившуюся библиотеку


    1. unreal_undead2
      04.06.2024 12:15

      Если обрабатываемые числа отличаются на десятки порядков, никуда не денешься.


      1. ptr128
        04.06.2024 12:15

        Попробуйте прибавить к 1E30 единицу в формате с плавающей запятой.

        А числа с фиксированной точкой могут оперировать и с сотнями порядков.


        1. unreal_undead2
          04.06.2024 12:15

          Одна единица на результат не влияет, если их много - см. текст статьи )

          А числа с фиксированной точкой могут оперировать и с сотнями порядков.

          Что при этом с производительностью?


          1. ptr128
            04.06.2024 12:15

            Одна единица на результат не влияет, если их много - см. текст статьи )

            Если речь о 1E30, то даже миллион единиц не повлияет. Так что если нужна точность - необходимо отказаться от плавающей запятой.

            Что при этом с производительностью?

            Если сравнивать просто SUM() в PostgreSQL, то на 30-35% медленней. Но если вместо SUM() попробовать алгоритмы из текста статьи, то фиксированная запятая сразу выигрывает с отрывом.

            Проверять на SIMD, если честно, в лом. Но есть обоснованные подозрения, что выиграет фиксированная запятая. За счет того, что сложение с фиксированной запятой в AVX-512 всегда один такт, а с плавающей - нет, так как требует предварительной нормализации.


            1. unreal_undead2
              04.06.2024 12:15

              Если речь о 1E30, то даже миллион единиц не повлияет.

              А вот 1E30 - повлияют, а это не такой уж большой размер данных, на одну ноду влезет.

              Если сравнивать просто SUM() в PostgreSQL

              Странный референс, я бы скорее на BLAS (или его аналоги и оптимизированные реализации от вендоров железа) смотрел.

              За счет того, что сложение с фиксированной запятой в AVX-512 всегда один такт, а с плавающей - нет

              Спецификации процессоров с вами не согласны. И не очень понимаю, как за один такт обработать операцию с фиксированной запятой с сотнями порядков.


              1. ptr128
                04.06.2024 12:15

                А вот 1E30 - повлияют, а это не такой уж большой размер данных, на одну ноду влезет.

                У Вас с арифметикой проблемы. 4E30 байт - это 4 кветтабайт. При том, что объем всей информации в мире оценивается, примерно, в 100 зеттабайт 1E23. В 10 миллионов раз меньше.

                Странный референс, я бы скорее на BLAS (или его аналоги и оптимизированные реализации от вендоров железа) смотрел.

                А я Вас не понимаю. BLAS - это просто библиотека. Как Вы в ней будете хранить большие массивы информации? Особенно, если Вы на кветтабайты замахиваетесь )))

                И не очень понимаю, как за один такт обработать операцию с фиксированной запятой с сотнями порядков.

                512-битное число с фиксированной запятой, вполне умещающееся в регистр ZMM, эквивалентно, примерно, 153 значащим десятичным цифрам. Гугол легко влезет. А больше и не надо, так как это намного больше количества элементарных частиц в видимой части вселенной.


                1. unreal_undead2
                  04.06.2024 12:15

                  4E30 байт - это 4 кветтабайт

                  Sorry, по основанию 2 считал )

                  BLAS - это просто библиотека. Как Вы в ней будете хранить

                  Мы вроде про стоимость вычислений говорим, а не про хранение.

                  512-битное число с фиксированной запятой, вполне умещающееся в регистр ZMM

                  То есть целый регистр (и соответствующее число операций) тратится на одно число вместо восьми даблов - причём явных операций для сложения 512-битных чисел нет, придётся ещё вручную с переносами разбираться.

                  А больше и не надо, так как это намного больше количества элементарных частиц в видимой части вселенной.

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


                  1. ptr128
                    04.06.2024 12:15

                    Мы вроде про стоимость вычислений говорим, а не про хранение.

                    Одно без другого не бывает. Особенно с учетом того, что в статье рассматриваются методы не последовательного суммирования.

                    Нравится Вам это или нет, но числа откуда-то берутся. И в большинстве случаев - именно из базы данных. Да и мне проще было сгенерировать таблички из случайных чисел и посчитать время по ним.

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

                    В любом случае, число больше гугола никакого смысла не имеет.


                    1. unreal_undead2
                      04.06.2024 12:15

                      Нравится Вам это или нет, но числа откуда-то берутся. И в большинстве случаев - именно из базы данных

                      Это могут промежуточные результаты вычислений в памяти (тот же дифур на большой сетке).


                      1. ptr128
                        04.06.2024 12:15

                        Я потому и написал, что "в большинстве случаев", а не всегда.

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

                        На таких объемах эти миллисекунды все рано роли не сыграют.


                      1. unreal_undead2
                        04.06.2024 12:15

                        Тут вопрос как на мир смотреть, для меня большие объёмы действительных чисел - это в первую очередь HPC и сложные расчёты.

                        На таких объемах эти миллисекунды все рано роли не сыграют.

                        Смотря насколько часто эти вычисления проделываются и сколько раз, миллисекунды могут и в часы сложиться (хотя и просто пробежаться по нескольким сотням гигабайт - уже не миллисекунды).


                      1. ptr128
                        04.06.2024 12:15

                        для меня большие объёмы действительных чисел - это в первую очередь HPC и сложные расчёты

                        Так по любому миллиарды чисел в оперативку не влезут. А миллионы суммировать - десятки миллисекунд.

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

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

                        Или Вы пытаетесь подменить тему, переходя от суммирования очень длинного вектора к совсем другим расчетам?

                        хотя и просто пробежаться по нескольким сотням гигабайт - уже не миллисекунды

                        Сотни гигабайт - это уже явно вектор не в памяти, а в БД. А вот миллисекунды или нет - зависит от мощности кластера.

                        А вообще Вы мне напоминаете экскурсовода, утверждающего, что этой окаменелости 200 000 005 лет, потому что пять лет назад ей было 200 миллионов лет. Точные вычисления - это не про дифуры, где исходные данные хорошо, если шесть точных знаков имеют.


                      1. unreal_undead2
                        04.06.2024 12:15

                        Так по любому миллиарды чисел в оперативку не влезут.

                        Берём, скажем, такую машинку - https://www.alcf.anl.gov/aurora - 512 Gb обычной DRAM на узле плюс ещё сколько то HBM, вполне хватит для миллиардов чисел.

                        переходя от суммирования очень длинного вектора к совсем другим расчетам?

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

                        Точные вычисления - это не про дифуры, где исходные данные хорошо, если шесть точных знаков имеют.

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