22.11.2021

Ryzen 7 5700U

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

Процессор относится к энергоэффективным процессорам второго поколения (Zen 2) для мобильных устройств, то бишь ноутбуков, с достаточно скромным теплопакетом в 25 Вт максимум. Кэша третьего уровня немного, всего 8 МиБ.
На борту 16 ГиБ памяти DDR4-3200.

А сейчас результаты тестов. За точку отчета (т. е. за единицу) взята производительность AMD FX-4350.

Сначала посмотрим на производительность операций с плавающей точкой.

 

В принципе, вроде достойно выглядит на фоне десктопного пятого Ryzen'а. Но с учетом чуть более быстрой памяти в ноутбуке я б ожидал большего.
Конечно, он быстрей стареньких Intel'ов, но они с более медленной памятью не сильно от него отстают. Видимо, более новые будут быстрее этого процессора от AMD, но не стоит забывать про ограниченный теплопакет. Процессоры от Intel гораздо более горячие, чем тестируемый, ну или могут быть (наверное?) такие же холодные, но и более медленные. Как-то так.


А вот во многопоточном режиме процессор особо не блещет. У него 8 ядер с поддержкой Hyper-Threading, но при этом он проигрывает десктопному варианту с 6  ядрами!
Видимо, тут сказываются ограничения теплопакета.
Процессоры Intel предыдущих поколений он, конечно, значительно опережает... Но у тех-то по четыре ядра в основном и память более медленная.

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

Теперь оценим многопоточное ускорение на разных размерностях задачи.



Как видим, "загадочный" горб для мобильного процессора на графиках ускорения имеет более выраженный характер.
На текущий момент мне кажется, что фронт этого горба связан с сочетанием сразу двух факторов: недостаточное количество памяти для генерации такого количества систем, что бы можно было более-менее точно замерить производительность на малых размерах задачи. Плюс потоки часто мешают друг другу получать системы для их решения, простаивая в очереди ожидания.
А вот более короткая полка по сравнению с Ryzen 5 связана явно с более маленьким размером кэш-памяти 3-го уровня (8 МиБ против 32 МиБ).
Зато на графике для 32-битных данных хорошо заметно, что память у ноутбука чуть-чуть быстрее (3200 МГц против 3000).

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


 


А вот тут мобильному процессору от AMD есть чему удивить. В однопоточной производительности он умудрился даже чуть-чуть обогнать десктопный вариант.
В многопоточном варианте он тоже самый быстрый, но совсем немного обгоняет Ryzen 5, несмотря на два дополнительных ядра.
Но это плата за более высокую энергеэффективность.

Выводы

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

20.09.2021

UInt128

Реализовал все методы для полноценной работы с беззнаковым целым длиной 128 бит (16 байт) на Pascal, реализация только для Delphi под X64, так как большая часть кода на ассемблере. Качайте UInt128.pas, кому надо.

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

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

19.09.2021

Обратный элемент

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

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

Итак, наиболее часто встречаемый вариант на целых числах основан на решении итерационным методом диофантова уравнения первого порядка:

Ax + By = R

Здесь A и B ‒ числа, для которых мы ищем частное решение (если R=0, то мы также можем найти наибольший общий делитель, который будет равен значению R с предыдущей итерации метода Евклида, если R=1, то мы находим как раз обратный элемент в кольце вычетов). А x и y ‒ это неизвестные целые коэффициенты, которые нужно найти, что бы выполнялось равенство.
В прошлых постах я несколько нестандартно трактовал их в обратном порядке, когда
x и y были числами, а A и B ‒ коэффициентами. Впрочем, так как они вполне симметричны, то суть дела это не меняет.

Для поиска обратного элемента удобнее в качестве A взять модуль, а в качестве B ‒ число, для которого мы ищем обратный элемент.
Тогда после решения диофантова уравнения
Ax + By = 1 y будет обратным элементом к B.
Не к каждому
B в кольце вычетом по модулю A может быть найден элемент. Это возможно только когда A и B является взаимно простыми числами. В противном случае в результате работы расширенного алгоритма Евклида мы скорее всего получим решение Ax + By = 0, т. е. R с предыдущей итерации будет НОД(A, B), а By mod A  = 0.

В алгоритме с целыми числами знак коэффициентов на каждой итерации меняется, поэтому если y получился меньше ноля, то в качестве обратного элемента к B мы должны взять A + y.

Вариант для натуральных чисел отличается от целых лишь уравнением:

Ax - By = R

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

Ax₁ - By₁ = R,
где 
x₁  = 1; y₁ = Q₁ = A div B; R₁ = A mod B,

То на второй

By₂ - Ax = R,

где y₂ и x считаются аналогично первой итерации.
Здесь тоже есть тонкость: так как на каждой нечетной итерации коэффициент при
B берется со знаком "минус", то обратный элемент должен считаться так: A - y, в то время как на четных итерациях он точно равен y.
Для расчета обратного элемента коэффициент
x нам не нужен, поэтому его и вовсе можно не считать.
А для
y можно использовать следующую рекуррентную формулу:

Кстати,
x считается точно по такой же формуле, но начальные значения поменяны местами.

В результате на чистом паскале получается так:

function getInvElNat(Module, El : UInt64) : UInt64;
var
  b : byte;
  Y1, Y0 : UInt64;
  D, A, R : UInt64;
begin
  A := Module;
  b := 0;
  Y1 := 0;
  Y0 := 1;
  while El > 1 do
  begin
    D := A div El;
    R := A - El*D;
    A := El;
    El := R;
    Result := Y0*D + Y1;
    b := not(b);
    Y1 := Y0;
    Y0 := Result;
  end;
  if b <> 0 then
    Result := Module-Result;
end;
 

По хорошему b должна быть типа boolean, но с точки зрения производительности разницы нет.

А вот так я переписал на ассемблере:

function getInvElNatASM(Module, El : UInt64) : UInt64;
// Module - RCX, El - RDX, Result - RAX
asm
  push RSI;
  mov RAX, Module;
  mov R8, El;
  mov R9, El;
  xor R10b, R10b; // b
  xor R11, R11; //Y0
  xor RSI, RSI; // Y1;
  inc R11;
  cmp R8, small 1;
  jbe @Finish;
@Loop:
  xor RDX, RDX;
  div R8;
  mov R9, R11;
  imul R9, RAX;
  add R9, RSI;
  not R10b;
  mov RAX, R8;
  mov R8, RDX;
  mov RSI, R11;
  mov R11, R9;
  cmp R8, small 1;
  ja @Loop;
@Finish:
  sub Module, R9;
  test R10b, R10b;
  cmovz RAX, R9;
  cmovnz RAX, Module;
  pop RSI;
end;

Получилось гораздо компактнее, чем целочисленный вариант с длинной арифметикой. Правда, производительность отличается не сильно:
целочисленный вариант на asm ‒ 10.7 млн. оп./с;
‒ натуральный вариант на asm ‒ 11.4 млн. оп./с;
‒ натуральный вариант на Pascal ‒ 7.8 млн. оп./с.

13.07.2021

MUL/IMUL

Итак, в прошлом посте я столкнулся с фактом, что полное 128-битное умножение примерно в 8 раз медленнее, чем 64-битное. Как-то медленновато будет!

В чем же дело? А дело, понятно, в коде. Вот в таком:

class operator UInt128.Multiply(Op1, Op2: UInt128): UInt128;
begin
  if Op1.HiPart = 0 then
    if Op2.HiPart = 0 then // малое умножение
      Result := SmallMultiply(Op1.LowPart, Op2.LowPart)
    else
      Result := Op2*Op1.LowPart
  else
    if Op2.HiPart = 0 then
      Result := Op1*Op2.LowPart
    else // большое умножение
      Result := LargeMultiply(Op1, Op2);
end;

Идея этого кода в следующем: невозможно оптимизировать весь код на ассемблере, оптимизировать надо только самые часто используемые части. А таким частями часто оказываются "листовые" функции, т. е. функции, которые не вызывают других функций.
Вот и здесь оператор умножения 128 на 64 бита у меня уже был реализован, добавил еще функции для малого и большого умножения.

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

Так вот, это не тот случай! "Листовые" функции слишком быстрые и поэтому затраты на их вызов получаются слишком велики.
Поэтому переписываю вышеприведенный оператор умножения на ассемблере без вызова функций.

Кстати, саму структуру этого оператора умножения я тоже не сам придумал, а подсмотрел у AMD в Software Optimization Guide.
У них есть пример 64-битного умножения в 32-битном режиме, собственно, его можно смело переносить в вариант 128-битного умножения на 64-битных регистрах. Естественно, с учетом того, что свободных регистров в 64-битном режиме поболе будет.
Естественно, в примере нет никаких вызовов вспомогательных процедур, просто переходы на разные участки кода, но смысл такой же.
Ну и надо сказать, что они там тоже активно IMUL используют для короткого умножения.

Тем не менее, я при переносе вышеприведенного кода на ассемблер пошел своим путем и использовал полную версию беззнакового умножения. Видимо, в силу упертого консерватизма.
И получил в результате примерно 151 млн. 128-битных умножений в секунду, ускорив исходный код в 1.7 раза. Неплохо!
Теперь 128-битное умножение всего лишь в 4.5 раза медленнее, чем 64-битное, что в принципе вполне приемлемо.

И вот я смотрю на код и вот не нравятся мне эти условные переходы. Думаю: "А что будет, если я вообще от них откажусь?"
Отказываюсь от проверок, тупо всегда умножая, как будто операнды полные , провожу несколько экспериментов с регистровой оптимизацией и еще немного ускоряю полное умножение!
В результате получаю 165 млн. оп./с или в 4.1 раза медленнее 64-битного умножения. Отлично!

Но, возможно, я вместе с водой выплеснул и ребенка? Ведь исходный пример от AMD не просто так был придуман и позволял снизить количество операций умножения, когда один или оба операнды имели данные меньше 64-бит длиной.
Поэтому тестирую вариант, когда один из операндов имеет 128-битные данные, а вот у второго данные помешаются в 64 бита.
С условными операторами это дает 159 млн. оп./с, что совсем не намного быстрее, чем когда оба оператора полноразрядные. А вот без условных операторов, когда всегда выполняется 128-битное умножение полностью, скорость возрастает до 168 млн. оп./с.
То есть полное умножение оказывается быстрее сокращенного!

Почему так? Видимо, из-за условных операторов и сброса конвейера. У меня есть предположение, что этот код из Optimization Guide кочует из одной версии руководства в другую, а написан он был давным-давно, в эпоху 386 или 486 процессоров.
Тогда операции умножения выполнялись гораздо дольше, (от 9 до 38 тактов на 486 процессоре), конвейер либо вообще отсутствовал, либо был очень короткий, а операции сравнения были почти такими же быстрыми, как и сейчас. Поэтому для такой архитектуры использование переходов в процедуре длинного умножения было вполне оправдано.
Но на современных процессорах команда умножения выполняется гораздо быстрее, а конвейер имеет значительную длину, так что его сброс стоит очень дорого. Так что, по всей видимости, для 128-разрядных умножений использовать условные переходы невыгодно.
За исключением, пожалуй, того случая, когда данные обеих операндов меньше 64 бит. К сожалению, оттестировать такой вариант очень сложно, так как результат умножения очень быстро растет.
Но, честно говоря, я не вижу смысла оптимизации именно для такого случая. Если данные столь короткие, то не проще ли для их обработки использовать 64-битный тип?
А если данные все же велики, то вероятность того, что оба операнда будут меньше 64 бит, мала и встречаться будет нечасто, так что потери производительности от такой ситуации будут крайне незначительны.

Ну и наконец про IMUL. Ну все же используют, а я чего-то не стал... Ну, попробую. Я, собственно, не ожидал какого-то увеличения производительности на моем PileDriver, исходя из производительности умножения (короткое и длинное умножение занимают одинаковое количество тактов на этой архитектуре). Да, получилось на одну команду меньше и регистры веселее использовались. Но вот скорость стабильно оказывалась на несколько процентов меньше, чем в случае полного умножения.
Не знаю, с чем это связано. Вообще даже идей нет. Допускаю, что на Intel и на AMD Ryzen с коротким умножением эта функция будет выполняться на те же несколько процентов быстрее. Но в целом это ничего не меняет, поэтому оставил в коде беззнаковую полную версию MUL.

20.06.2021

Умножение UInt128

Добрался наконец-то до умножения. Решил его сделать хитро, отдельно выделив умножение на небольшие целые, с целью оптимизации в первую очередь операций по переводу из разных систем счисления.
Такое умножение можно заменить несколькими операциями сложения и сдвига, думал, что получится быстрее. Но нет, быстрее не получилось, почти одинаково: видимо, лишнее сравнение и косвенный вызов процедуры съедают весь выигрыш. Впрочем, на больших размерностях эффект должен быть более заметен.

Тестирование проводил на нечетных числах, так как умножение на четные быстро обнуляет один из операндов на ограниченном по точности типе.
В результате Pascal в релизе показал 680 млн. 64-битных умножений в секунду, а мое умножение 128-битного на 64-битное - всего около 170 млн. Примерно в 4 раза медленнее. Хотя должно бы быть теоретически медленнее всего чуть больше двух раз. Такова плата за универсальность.

А вот умножение 128 на 128 бит дает 88 млн. умножений в секунду, что уже в 8 раз медленнее чисто 64-битного умножения, что странно. Ведь такая 128-битная операция требует лишь три 64-битных умножения и два сложения. Надо внимательнее посмотреть на свой код, видимо, там можно что-нибудь оптимизировать, я даже догадываюсь, что 😁.

Оценить, насколько хорошо работает PascalABC.NET с его BigInteger достаточно сложно. Дело в том, что он имеет "бесконечную" точность, рассчитывая полное произведение, в том время как мой тип старшую часть, выходящую за 128 бит, теряет. В результате множитель в процессе теста на BigInteger постоянно и очень быстро растет, а вместе с тем растет и вычислительная сложность каждого умножения.
В результате я замерял его производительность так: сначала замерил время выполнения N умножений, отбрасывая после каждого все то, что выходило за пределы 128 бит. с помощью побитового AND.
Затем замерил время N операций AND, и вычел одно время из другого.

В результате BigInteger показал 5.5 млн. умножений в секунду, или в 16 раз медленнее моего варианта 128 на 128 бит. Я считаю, что очень неплохо, с учетом того BigInteger считал полное умножение, получая 256 бит результата, что требует еще одного 64-битного умножения и кучи сложений.
Поначалу я предполагал, что в BigInteger используется универсальный алгоритм умножения, например, методом Фурье, но, судя по всему, там используют разные алгоритмы в зависимости от длины операндов. И на относительно коротких умножениях они используют обычный вариант длинного умножения.

25.05.2021

UInt128

Решил как-то на досуге накидать тип для удобной работы с длинным беззнаковыми целыми. На плюсах перегрузка операций была изначально, а вот в Object Pascal ее добавили позже, причем с ограничениями. В частности, для перегрузки операций можно использовать только записи (record), но не классы.

В процессе написания, естественно, возникло желание потестить на производительность, пока что только операции сложения/вычитания. Но вот с чем сравнивать результат? Подумал и решил, что можно сравнивать производительность со встроенным 64-битными беззнаковыми целыми (UInt64).
Логика тут такая: Delphi генерирует код в нативных командах процессора, одно сложение - одна команда. А при реализации своего типа с перегруженными операциями взамен нативных команд будет вызов процедуры.
Таким образом, одна команда сложения для UInt64 в случае UInt128 будет заменять на вызов процедуры с 4 обращениями к памяти и двумя командами сложения. Примерно прикинул и подумал, что троекратное снижение производительности в этом случае будет нормально.

Запускаю тест на отладочной версии и получаю следующие результаты: UInt64 на pascal дает примерно 533 млн. сложений в секунду, а UInt128 ‒ 247 млн. Вроде бы не плохой результат: замедление не в 3, а всего лишь в 2.15 раза.
Думаю, а что будет, если переписать UInt64 на assembler? Недолго думая делаю и получаю 3 млрд. 600 млн. регистровых операций в секунуду! Подумать только! 3.6 млрд. 64-разрядных сложений в секунду, Карл! И это старенький, не топовый процессор. Что же сейчас творят топы!?

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

И на что тратится это гигантская производительность? Похоже, в основном на то, что бы красиво размывать фон на фотографиях котиков в модных соц. сетях. 😆

С чем еще можно померится производительностью? Вспоминаю, что в Java и в .NET есть встроенный тип BigInteger. Он, правда, весьма универсальный, так как имеет "бесконечную" точность, в отличие от того типа, что делаю я, да еще и знаковый. Но мне интересно, сколько стоит такая универсальность?

На Java писать под Windows мне совсем не хочется, даже консольное приложение, но можно вот взять PascalABC.NET, под него и код-то особо переписывать не нужно. Естественно, организую тест так, что бы данные BigInteger занимали больше 8 байт, но меньше 16, что бы сравнение было корректным.
Вот результаты: UInt64 ‒ 477 млн. операций в секунду. Честно говоря, очень достойный результат для .NET, Delphi в отладочной версии дает чуть больше. А вот BigInteger уже не так хорош: в районе 105 млн. операций в секунду. То есть примерно в 2.5 раза хуже, чем мой тип на Delphi.
Думаю, не такая уж большая цена за универсальность. Но иногда производительность все же важнее.

А потом я компилирую на Delphi вместо отладочного варианта релиз и удивляюсь: код для UInt64 на pascal оказывается быстрее кода на assembler ‒ 4 млрд. 179 млн. сложений! Видимо, Delphi все-таки немного умеет в оптимизацию: в цикле я развернул сложения по 10 штук, что бы снизить погрешность измерения от кода, организующего сам цикл.
Видимо, оптимизатор просек эту тему, и заменил 10 команд сложения на одну. Но это не точно: код в релизе без отладочной информации, а копаться в исходных кодах всех прочих библиотек, которые задействованы по умолчанию в работе программы у меня желания нет.
Но хочу заметить, что .NET так не умеет!

Впрочем, сложение ‒ это так себе тема, гораздо интереснее будет, когда я доберусь до умножения, а особенно ‒ до деления.

26.04.2021

GPS L5

Я тут несколько лет назад как-то уже затрагивал тему GPS. Однажды, прогуливаясь по горам Черногории, решил записать GPS-трек, благо программка на смартфоне готовая уже была. Прошли мы тогда с женой немало, треккер выдал 15 км.

Однако, приехав домой, я обнаружил, что записанный трек никуда не годиться. Так как гуляли мы в гористой местности, то, если верить треккеру, благодаря отражению сигнала GPS от гор, приобрели магическую способность мгновенно перемещаться на противоположный склон, а потом сразу назад. Поэтому прошли мы гораздо меньше, наверное, около 10 км или чуть больше. Но самое главное, делится треком не было никакого смысла.

В результате родилась у меня идея написать самому простенький треккер, но только такой, который бы откидывал недостоверные точки на маршруте. Заодно немножко погрузился в тему точности GPS-позиционирования и выяснил,что начиная с версии спутников GPS-IIF они передают сигнал в полосе L5, которая обещала прямо чудеса позиционирования: ошибку менее метра и частоту определения координат 5 раз в секунду.

А тут у меня как раз начал потихоньку морально устаревать смартфончик Alcatel, в связи с чем я с трудом, но все-таки задавил жабу  и купил новый смартфон с поддержкой L5. С трудом, потому что в основном пользуюсь смартфоном как телефоном, да и то не очень часто.

В общем, не прошло и года пары лет после покупки смарта, как я вернулся к идее написать треккер. Скопировал код из программки-акселерометра и давай тестить. С печалью обнаружил, что никаких 5 отчетов в секунду нет и рядом. Да и точность, которую мне возвращал Android, лучше не стала, а как бы даже наоборот.
Впрочем, программки, которым требовался GPS, стали работать гораздо лучше. Долго я копался, пытаясь понять в чем дело. Думал, может L5 надо как-то специально активировать. Потом предположил, что в России из-за параноидальных требований государства может производители отключают. В общем, информации очень мало, никто в тему особо не погружается, очень она специфична.

В конечном итоге всё оказалось проще (но это не точно): полез в справочник по API. И тут-то выяснилось, что оказывается то, что я использовал, успело устареть. Но тем не менее исправно работало.
Переписал на новый API. И тут меня ждала частичная удача. На новом API опрос датчиков происходил гораздо чаще, максимальная частота - 3 раза в секунду, но не стабильно, все-таки чаще в районе 1 раза в секунду. 5 Гц пока так ни разу и не увидел. Не знаю почему, может новых спутников пока не достаточно, или еще в чем-то дело.
Точность новое API выдает такую же, как и старое и не лучше, чем было на стареньком Alcatel. Тем не менее, точки лежат более ровно, сильных выбросов в стороны от траектории сейчас нет.
Есть лишь один вопрос (из многих) к Гугл: если старое API продолжает работать, то почему оно не может возвращать данные чаще? Никаких принципиальных преград я к этому не вижу.

Тестирую свое приложение дальше и обнаруживаю, что оно не работает в фоновом режиме. Трачу кучу времени на то, что бы понять, где я налажал в программе. В конечном итоге выясняется, что начиная вроде с 29 версии Android они ввели дополнительное разрешение, которое нужно запрашивать у пользователя на опрос GPS в фоновом режиме.
Ладно, добавляю это разрешение. Не работает в фоне! Копаюсь дальше. И выясняю, что начиная с 30 версии (на которую я как раз за пару недель до этого обновил смартфон) этого разрешения недостаточно! Для каждой такой программы нужно запрашивать специальное разрешение у Гугл, разрешение пользователя недостаточно!

То есть я специально купил устройство, которое имеет необходимое для меня оборудование и возможности, сам написал для него программку для индивидуального использования, но не могу использовать её даже в отладочном режиме! Это просто апофеоз свободы! И все это ради нашей безопасности! )))

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

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

23.01.2021

Отгадка

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

Между тем, есть очень простой способ проверить, что является причиной – взять, да и отключить HyperThreading. Что я собственно и сделал на AMD Ryzen 3600.
Отключить-то я его отключил, а вот назад он включаться не захотел, да. На материнке ASRock B450M Steel legend. Давненько я с такими глюками не сталкивался.
Погуглил. Простые советы не помогли, оставался последний способ - сбросить BIOS. И тут меня перемкнуло, и я решил сбросить BIOS жестко, перемкнув соответствующие контакты на материнской плате. Перемычки под рукой не оказалось, поэтому замыкал контакты имеющимися под руками металлическими предметами, строго по инструкции, на 5 секунд.
Несколько попыток не помогли
– BIOS не сбрасывался. Я уж чуть было не разобрал полкомпьютера, пытаясь добраться до батарейки CMOS-памяти, да вовремя остановился. Достаточно оказалось сбросить BIOS программно.😅

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

Посмотрим графики. "12/12" – 12 потоков на 12 виртуальных ядрах из 6 физических; "6/6" 6 потоков на 6 физических ядрах; "6/12" 6 потоков на 12 виртуальных ядрах.
Кстати, там есть любопытный момент.



Сначала банальность: в однопоточном режиме HyperThreading никак не влияет на производительность.

В многопоточном HyperThreading безусловно хорошо повышает общую производительность, пока решаемая задач целиком попадает в кэш L3.
Но при этом производительность каждого потока оказывается немного меньше, чем при отключенном HyperThreading. Насколько меньше? Примерно на 4-5% для каждого потока, и до 30% на шести потоках в целом (по одному потоку на физическое ядро).
Это хорошо видно на графиках "6/62 и "6/12". Последний проигрывает первому на всех размерностях.

О чем это говорит? О том, что без HyperThreading отзывчивость приложения может быть лучше. Потенциально до 30%, но гораздо ближе к практике  5% в силу параллельности решения задачи.
Сомневаюсь, что эти 5% заметит даже самый требовательный игрок, но в каких-то вариантах эти 5% могут все же иметь значение.

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

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

09.01.2021

Результаты вещественного теста

Ну вот, вроде бы оттестировал все процессоры на решение систем линейных уравнений методом Гаусса-Жордана, самое время привести результаты.

Здесь я не стал приводить результаты AMD FX-4350, так как он является базой для сравнения и его производительность принимается за единицу.
Как видно, рост
ускорения на вещественных операциях  за несколько лет составил даже больше, чем в два раза.
Безусловный лидер среди оттестированных – это AMD Ryzen 5 3600, он уверенно обгоняет оттестированные процессоры Intel поколения SkyLake.

 
Самое интересное, что и в однопоточном режиме, в отличие от целочисленных операций (см. предыдущий пост), рост производительности за несколько лет весьма значителен: 60% скалярной производительности и более 150% векторной.
Складывается впечатление, что основное ускорение в будущем в однопоточной производительности будет именно на вещественных операциях, так как, похоже, потенциал серьезного ускорения целочисленных операций на текущий момент полностью исчерпан. Хорошо, если будет один-два процента за год.
Впрочем, посмотрим, что даст в отношении целочисленных операций M1 от Apple. Вдруг случится неожиданный сюрприз. 😀

 
При одновременной работе нескольких потоков тупо всё решает их количество. Так что "щедрый" на ядра подход AMD, похоже, себя оправдывает.

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

 
Как видно, процессоры делятся на две группы: у одной серьезный провал на минимальном размере, у некоторых  процессоров Intel получается даже медленнее, чем решать на одном ядре.
У второй же группы провал отсутствует или минимален.

В первой группе все процессоры, поддерживающие HyperThreading, кроме Intel i3-3227U. Этот процессор, хоть и имеет данную технологии, не демонстрирует просадку производительности на минимальном размере.

Как одну из причин такого странного поведения я изначально рассматривал нехватку ОЗУ, что при переходе на многопоточный тест, из-за высокой производительности процессоров, не давало возможности сгенерировать достаточное количество систем для теста, что снижало точность тестирования.
Так, Intel i7-6700HQ имеет 12 ГиБ ОЗУ, i5-8300H вообще 8, а Ryzen 5 3600 16, что с учетом его производительности может быть недостаточно.

Но с другой стороны, на типе double провал существенно меньше, хотя производительность уменьшается незначительно (буквально на несколько процентов), в то время как памяти требуется в два раза больше. Если бы дело было в нехватке ОЗУ, это должно было бы привести к еще большему провалу, но это не так.


Видимо, все же дело в HyperThreading? Надо подумать, как можно было бы это легко проверить.

Да, еще: Scalar SSE всегда быстрее FPU. Ну, почти всегда. На трех переменных у Intel таки быстрее FPU. Но такое преимущество только на простых, арифметических операциях.
Сохранится ли оно на более сложных, например, тригонометрических, степенных или логарифмических операциях? Данный вопрос требует отдельного исследования, но у меня пока ни одной интересной задачки на эту тему не вырисовывается.
Ну и жертвуется точность, конечно. В редких случаях это может быть важно. Впрочем, пока что FPU присутствует на всех современных процессорах, так что при необходимости всегда можно его использовать.