Comments 98
PHP вообще один из самых быстрых интерпретируемых языков
Ну тогда и Java тоже называйте «интерпретируемым» языком. Ведь в ней, как и в PHP:
- Исходный код компилируется в байт-код
- Байт-код выполняется на виртуальной машине
- Есть JIT в нативный код
Ну а в чем принципиальная разница? Кроме того, что PHP не компилирует неиспользуемые файлы?
PHP на данный момент это язык со слабой динамической типизацией, стандартная реализация которого является компилируемой.
PHP на данный момент это язык со слабой динамической типизацией
Так было лет 10 назад. Сейчас это язык с опциональной сильной и опциональной статической на уровне объектной модели.
Т.е. это вообще единственный ЯП, который я знаю, где можно самостоятельно выбрать как писать.
Ага. Ну а чем не кейс сильной и статической?))
С другой стороны, если совсем упороться...
function &int(int $initial = 0): int
{
static $values = [];
$values[] = new class($initial) {
public function __construct(public int &$initial) {}
};
return $initial;
}
$int = &int(42);
$int = 'sdsd'; // Error: Cannot assign string
Я имею в виду, написать что-то типа
class Foo {
private Collection $prop = [];
}
при условии, что конструктор класса Collection может принять массив, как аргумент?
Может через атрибуты как-то попробовать, придумать какой-нибудь #[Init]?
Но аргументы атрибутов тоже должны быть компайл-тайм константами…
Но аргументы атрибутов тоже должны быть компайл-тайм константами…
И это ограничение тоже можно обойти...
$fp = fopen('php://memory', 'rb');
$fp = stream_context_set_option($fp, [
'php' => [ 'some' => new AnyNonConstantValue() ]
]);
define('VALUE', $fp);
//
class Some
{
#[AttributeName(value: VALUE)]
public Collection $prop;
}
// Теперь внутри value атрибута будет лежать ресурс, внутри которого лежит любой произвольный объект.
// Такие дела...
Но я восхищен
Ну вообще, даже такие "грязные хаки" можно вполне легально использовать. Например, вот в этой либе https://github.com/phpfn/symbol подобный приём используется для реализации псевдотипа "символов", который в свою очередь используется для реализации функционала каррирования, пайпинга и частичного применения (На нынешних обсуждениях RFC для подобного функционала на уровне языка предлагается использовать литерал ?
).
Найдите соответствующие опции JVM, реально для интерпретации, и запустите ваш class ('байт-код') - можно будет очень-очень долго ждать (но это будет 'выполнение байт-кода').
В современных JVM давно по умолчанию JIT. И ещё не поленитесь реализовать 'прогрев' теста, там есть профайлер. Удачи...
Ну тогда и Java тоже называйте «интерпретируемым» языком.
И это будет неправдой. Просто потому что у Java языка есть своя спецификация а у байткода JVM своя и по сути это две разные вещи.
Кстати сам байткод это не обязательно продукт компиляции Java кода и он по сути является интерпретируемым «языком»(ну если ассемблер называют языком программирования то почему бы и байткод так не называть?). А вот язык Java требует обязательную стадию компиляции так что назвать его интерпретируемым не получится.
Сам байт-код является по сути «интерпретируемым» на виртуальной машине.
И можно себе представить любой другой язык, который компилируется в оп-коды ZE.
Язык PHP требует обязательную стадию компиляции. И назвать его интерпретируемым не получится.
Так в чем разница?
Согласно en.wikipedia.org/wiki/Interpreter_(computing):
An interpreter generally uses one of the following strategies for program execution:
- Parse the source code and perform its behavior directly;
- Translate source code into some efficient intermediate representation and immediately execute this;
- Explicitly execute stored precompiled code[1] made by a compiler which is part of the interpreter system.
В случае PHP это чаще всего пункт номер два, возможно в прошлом первый пункт. Если имеется в виду имплементация с промежуточной компиляцией с помощью отдельной тулзы(возможно HipHop так работает, я не спец) то ее можно и нужно называть компилируемой.
В случае Java ни один из этих пунктов не подходит, ее компилятор не является частью интерпретатора. Если существует имплементация JDK которая работает без использования javac то ее можно и нужно называть интерпретируемой.
upd: в гитхабе нашел ответ на свой вопрос: 2.7.0
Тогда вопрос почему не 3? которая вроде как быстрее 2?
Несмотря на недочеты (маленькое время теста, отсутствие прогрева, маленькая выборка...), это отличный пост и очень интересный график.
Искал в табличке результаты Haskell.
Хорошо, жду. Накинул звезду.
Поправьте, пожалуйста, этот код. Так как вы нигде не указали ни одного типа, все числа дефолтятся до Integer
aka BigInt
, что абсолютно убивает производительность.
ghc c -Wall
предупреждает о дефолтинге:
PrimesSlow.hs:24:16: warning: [-Wtype-defaults]
• Defaulting the following constraints to type ‘Integer’
<...>
|
24 | if j == 2
| ^^^^^^
PrimesSlow.hs:31:28: warning: [-Wtype-defaults]
• Defaulting the following constraints to type ‘Integer’
<...>
|
31 | let primeNumberCount = firstOrDefault args 100
| ^^^^^^^^^^^^^^^^^^^^^^^
PrimesSlow.hs:34:47: warning: [-Wtype-defaults]
• Defaulting the following constraints to type ‘Integer’
<...>
|
34 | putStrLn ("The latest prime number: " ++ (show number))
| ^^^^^^^^^^^
Поправить можно просто написав сигнатуры с Int
import System.Environment (getArgs)
firstOrDefault :: [String] -> Int -> Int
firstOrDefault args defaultValue =
if length args == 1
then read (head args)
else defaultValue
getJ :: Int -> Int -> Int -> Int
getJ j i number =
if i > number
then j
else if number `rem` i == 0
then getJ (j + 1) (i + 1) number
else getJ j (i + 1) number
getNumber :: Int -> Int -> Int
getNumber primeNumberCount number =
if primeNumberCount == 0
then number
else do
let numberInc = number + 1
let j = getJ 0 1 numberInc
if j == 2
then getNumber (primeNumberCount - 1) numberInc
else getNumber primeNumberCount numberInc
main :: IO ()
main = do
args <- getArgs
let primeNumberCount = firstOrDefault args 100
let number = getNumber primeNumberCount 0
putStrLn ("The latest prime number: " ++ (show number))
-real 1m0.961s
-user 1m0.849s
-sys 0m0.108s
+real 0m18.971s
+user 0m18.965s
+sys 0m0.000s
Спасибо.
Хм, меня наоборот удивляет всего трехкратное ускорение. У меня под рукой только ноутбучный райзен, для бенчмарков такое себе, но на нем после этого изменения хаскель выполняется в районе c++/asm.
> docker build -t benchmark . > /dev/null && \
> docker run --rm benchmark ghc --version && echo '' && echo '' && \
> docker run --rm --volume $(pwd):/app benchmark ghc -O /app/prime-number/haskell/cmd.hs -o /app/prime-number/haskell/cmd.hs_bin > /dev/null && \
> docker run --rm --volume $(pwd):/app benchmark bash -c 'time /app/prime-number/haskell/cmd.hs_bin 7000'
The Glorious Glasgow Haskell Compilation System, version 8.6.5
The latest prime number: 70657
real 0m9.872s
user 0m9.863s
sys 0m0.003s
> docker build -t benchmark . > /dev/null && \
> docker run --rm benchmark nasm --version && echo '' && echo '' && \
> docker run --rm --volume $(pwd):/app benchmark nasm -f elf -Ox /app/prime-number/assembler/cmd.asm && \
> docker run --rm --volume $(pwd):/app benchmark ld -O3 -m elf_i386 /app/prime-number/assembler/cmd.o -o /app/prime-number/assembler/cmd.asm_bin && \
> docker run --rm --volume $(pwd):/app benchmark bash -c 'time /app/prime-number/assembler/cmd.asm_bin 7000'
NASM version 2.14.02
The latest prime number: 000070657
real 0m9.867s
user 0m9.860s
sys 0m0.001s
> docker build -t benchmark . > /dev/null && \
> docker run --rm benchmark g++ --version && echo '' && echo '' && \
> docker run --rm --volume $(pwd):/app benchmark g++ -O0 /app/prime-number/c++/cmd.cpp -o /app/prime-number/c++/cmd.cpp_bin && \
> docker run --rm --volume $(pwd):/app benchmark bash -c 'time /app/prime-number/c++/cmd.cpp_bin 7000'
g++ (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0
Copyright (C) 2019 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
The latest prime number: 70657
real 0m9.850s
user 0m9.841s
sys 0m0.002s
Если у Вас все еще "дело вечером, делать нечего", не могли бы Вы приложить вывод
ghc -O /app/prime-number/haskell/cmd.hs -o /app/prime-number/haskell/cmd.hs_bin -ddump-simpl -ddump-stg -ddump-cmm -ddump-asm -fforce-recomp -dsuppress-all -dno-suppress-idinfo -dno-suppress-type-signatures
/app/prime-number/haskell/cmd.hs_bin 7000 +RTS -s
Было бы интересно попробовать разобраться, в чем там дело.
Неплохо бы на всякий случай попробовать компилировать с -O2
вместо -O
, хотя у меня это вроде ни на что не влияет.
Еще можно вместо возврата j
из getJ
сразу сравнивать j
с двойкой и вызывать getNumber
. Этим убирается нехвостовой вызов getJ
из getNumber
, ассемблер становится куда красивее, но разницы по времени у себя я опять же не заметил. Ну и не знаю, не слишком ли это оптимизация под конкретный язык. Хотя как вообще определять что тут оптимизированная версия, а что нет, если обе серьезно отличаются от оригинала, так как циклов нет.
import System.Environment ( getArgs )
getNumber :: Int -> Int -> Int
getNumber primeNumberCount number = if primeNumberCount == 0 then number else calcJAndRecurse 0 1
where
numberInc = number + 1
calcJAndRecurse :: Int -> Int -> Int
calcJAndRecurse j i
| i > numberInc = getNumber (primeNumberCount - if j == 2 then 1 else 0) numberInc
| numberInc `rem` i == 0 = calcJAndRecurse (j + 1) (i + 1)
| otherwise = calcJAndRecurse j (i + 1)
main :: IO ()
main = do
args <- getArgs
let primeNumberCount = if null args then 100 else read $ head args
putStrLn $ "The latest prime number: " <> show (getNumber primeNumberCount 0)
The Glorious Glasgow Haskell Compilation System, version 8.6.5
The latest prime number: 70657
58,440 bytes allocated in the heap
3,576 bytes copied during GC
44,672 bytes maximum residency (1 sample(s))
24,960 bytes maximum slop
0 MB total memory in use (0 MB lost due to fragmentation)
Tot time (elapsed) Avg pause Max pause
Gen 0 0 colls, 0 par 0.000s 0.000s 0.0000s 0.0000s
Gen 1 1 colls, 0 par 0.000s 0.000s 0.0001s 0.0001s
INIT time 0.000s ( 0.000s elapsed)
MUT time 18.919s ( 18.923s elapsed)
GC time 0.000s ( 0.000s elapsed)
EXIT time 0.000s ( 0.000s elapsed)
Total time 18.920s ( 18.924s elapsed)
%GC time 0.0% (0.0% elapsed)
Alloc rate 3,088 bytes per MUT second
Productivity 100.0% of total user, 100.0% of total elapsed
real 0m18.925s
user 0m18.917s
sys 0m0.004s
Спасибо, но это только вывод выполнения, а тут самое интересное — дампы компилятора. Вы похоже их по привычке отправили в /dev/null (
Да, не знаю даже что и думать. Асм совпадает один-в-один, так что если только какое-нибудь выравнивание влияет, но я что-то сомневаюсь что этим можно объяснить такую огромную разницу.
Если Вам все это еще не надоело, было бы интересно посмотреть что сделает llvm бекенд, и как будет вести себя мой слегка оптимизированный код выше.
llvm можно поставить аптом,
RUN apt install -y llvm-10
И запускать как-то так:
docker build -t benchmark . > /dev/null && \
docker run --rm benchmark ghc --version && echo '' && echo '' && \
docker run --rm --volume $(pwd):/app benchmark ghc -O2 /app/prime-number/haskell/cmd.hs -o /app/prime-number/haskell/cmd.hs_bin -fllvm -pgmlo=opt-10 -pgmlc=llc-10 -optlo="-O3" -optlc="-O3" > /dev/null && \
docker run --rm --volume $(pwd):/app benchmark bash -c 'time /app/prime-number/haskell/cmd.hs_bin 7000'
PS: Было бы неплохо если кто-нибудь еще прогнал бенчи у себя на машине, а то мне уже начинает казаться что у меня какой-то специальный процессор для хаскеля стоит
Там надо 4 варианта: {opt, unopt}* {lazy, strict}
Про Crystal не забудь пожалуйста
Недавно была задача (помогал человеку с дипломом) - переписать через зад сделанную прогу с шарпа (которого, кстати, нет в статье) на питон. Прога состояла чисто из циклов и проверок if-else. При этом я умудрился убрать кучу ненужного кода и провёл все оптимизации, какие смог. Всё равно итоговое детище работало раз в 10 медленнее изначального...
Чисто теоретически — возможно-ли оптимизировать компилятор, чтобы он выдавал наиболее оптимальный код именно для таких простейших тестов? При этом для реально существующих задач код оставался неоптимальным.
Отлично, теперь я знаю, на чем искать простые числа
ассемблер будет быстрее Си
Вот как раз насчет таких заблуждений я и написал данную статью. Разница между ассемблером и Си — на уровне погрешности (если брать в сравнение результаты с флагами оптимизации). Конечно, если использовать особенности языка и аппаратной платформы, то здесь без вариантов. Но не стоит забывать что и на Си можно использовать свои лазейки. А время затраченное не написание кода — просто не сравнимо.
Современные компиляторы могут делать удивительные вещи.
PS. Первоначальный вариант на Assembler/NASM был медленнее Си где-то на 15%. И все из-за того что в регистрах были не те переменные.
$ time -p ./prime 100
The latest prime number: 541
real 0.00
user 0.00
sys 0.00
$ time -p ./prime 1000
The latest prime number: 7919
real 0.08
user 0.06
sys 0.00
$ time -p ./prime 10000
The latest prime number: 104729
real 5.30
user 5.30
sys 0.00
собрано так
$ icc -O3 -xHost -qopt-report=5 -qopt-report-phase=vec prime.cpp -o prime
вот что репортит компилятор
LOOP BEGIN at prime.cpp(16,9)
remark #15305: vectorization support: vector length 8
remark #15309: vectorization support: normalized vectorization overhead 0.317
remark #15355: vectorization support: j is int type reduction [ prime.cpp(14,15) ]
remark #15300: LOOP WAS VECTORIZED
remark #15475: — begin vector cost summary — remark #15476: scalar cost: 30
remark #15477: vector cost: 10.250
remark #15478: estimated potential speedup: 2.830
remark #15482: vectorized math library calls: 1
remark #15488: — end vector cost summary — LOOP END
Как видим векторизация цикла хорошо ускоряет код. Это то, что в основном недоступно в скриптовых языках.
Современные компиляторы (нативных языков типа С или Фортрана) непросто обогнать даже на ассемблере. Те времена, когда можно было легко получить 15% давно в прошлом.
У меня про C# vs C++ публикация, кстати, есть:
https://habr.com/ru/post/532442/
Для более корректного сравнения стоит добавить тесты с использованием luajit и PyPy (даже без изменений кода)
на моей машине запуск lua кода через luajit работает в 5 раз быстрее интерпретатора (9s vs 46s) и всего в 2 раза медленнее С кода (4.5s)
На моей машине почему LuaJIT получил быстрее С/C++ и Asm.
Пока не могу понять в чем фишка. Есть идеи?
хотя если так, то ответ был бы другой.
у меня luajit быстрее С с -O3 никогда не считал такие вот числодробилки (интегралы, сортировки) быстрее. на трех разных машинах.
я начал было думать, что в luajit-2.1.0-beta3 какие-то особые оптимизации по сравнению с luajit-2.0.4, установил, попробовал — также
почитал про кэши, про предсказания ветвлений в процессорах, прикинул кол-во операций (2.5млрд взятия остатков, чуть больше сложений) — отбросил эти мысли. если процессор хорошо оптимизирует переходы и встраивается в вычисления (сам выполняет преждевременный выход из цикла, когда нет смысла дальше считать), то ускорение должно быть сильно больше, чем получено в итоге.
могу лишь предложить протестировать в одинаково холодных условиях С optimized и luajit версии, желательно с разницей во времени в несколько минут.
про авторазгон частоты процессора ничего не читал, но подозреваю что такое может существовать и сильно влиять на результаты времени вычислений.
решил проверить на двух VPS'ках:
первая VPS:
:$ lscpu
Architecture: i686
CPU op-mode(s): 32-bit, 64-bit
Byte Order: Little Endian
Address sizes: 40 bits physical, 48 bits virtual
CPU(s): 1
On-line CPU(s) list: 0
Thread(s) per core: 1
Core(s) per socket: 1
Socket(s): 1
Vendor ID: GenuineIntel
CPU family: 6
Model: 45
Model name: Intel(R) Xeon(R) CPU E5-2620 0 @ 2.00GHz
Assembler/NASM — 15.0s
Assembler/NASM (optimized compilation) — 15.0s
Lua — 1m49s
Lua (LuaJIT) — 26s
вторая VPS:
$ lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Byte Order: Little Endian
Address sizes: 48 bits physical, 48 bits virtual
CPU(s): 2
On-line CPU(s) list: 0,1
Thread(s) per core: 2
Core(s) per socket: 1
Socket(s): 1
NUMA node(s): 1
Vendor ID: AuthenticAMD
CPU family: 23
Model: 1
Model name: AMD EPYC 7571
Assembler/NASM — 14.0s
Assembler/NASM (optimized compilation) — 14.0s
Lua — 1m20s
Lua (LuaJIT) — 8s
Если в системе только 1cpu — все норм, LuaJIT не быстрее Assembler, но теряется все тесты с *optimized compilation* — вообще без эфекта на производительность (вроме раста)
Как только 2+ cpu — LuaJIT становится быстрее Assembler. Все остальные JIT — норм. Но всегда работает только одно ядро (--cpus=«1» — не влияет)
PS. Первая мысть у меня — *предсказания ветвлений*, но пока не знаю как проверить или опровергнуть эту теорию (больно странный результат). Прогретые кеши не дадут такого прироста к скорости
1. первые несколько четных значений они реально вычисляют и видя что четные значения не влияют на primeNumberCount и видя, что цикл внутри одинаковый, а считать ещё долго, выкидывают проверку четных чисел
2. остаток деления нечетного числа на четное не может быть 0, сколько ни старайся.
в итоге и получается, что без этих оптимизаций luajit работает в 2 раза медленнее, а с ними в 2 раза быстрее.
если бы дело было только в оптимизациях процессора, то тогда результаты по ассемблеру на втором процессоре показали бы более привлекательные результаты. но если дело только в компиляторе, то почему он не оптимизирует этот же алгоритм в первом случае? и на других машинах?
могу предположить, что дело в векторизации (которую делает luajit после анализа), но в этих делах моя «экспертность» заканчивается и дальше начинаются «а вот может быть вот так, а может и не так. а может по-другому»
На месте разработчиков компиляторов я сделал бы такие же оптимизации. Хоть это и очень редкая задача. такое должно, по-хорошему, оптимизироваться на уровне исходного кода программистами…
итогового ответа у меня пока нет, нужны более осведомленные люди или какое-то время, пока я не разберусь (а разобраться в этом интересно)
g++ test.c -Ofast -std=c++11 -o test
#include <stdio.h>
#include <stdlib.h>
#include <cstdint>
int main( int argc, char *argv[] )
{
uint16_t primeNumberCount = 100;
if (argc == 2) {
primeNumberCount = atoi(argv[1]);
}
uint32_t keeper = 0;
uint32_t number = 0;
while (primeNumberCount > 0) {
++number;
uint16_t j = 0;
for (uint32_t i = 1; i <= number; ++i) {
if (number % i == 0) {
++j;
}
}
if (j % 2 == 1) {
++keeper;
}
if (j == 2) {
--primeNumberCount;
}
}
printf("The latest prime number: %d\n", number);
printf("Keeper: %d\n", keeper); // usage
}
local primeNumberCount = 100;
if #arg == 1 then
primeNumberCount = tonumber(arg[1]);
end
local number = 0;
local keeper = 0
while primeNumberCount > 0 do
number = number + 1
local j = 0;
for i = 1, number do
if number % i == 0 then
j = j + 1;
end
end
if (j % 2) == 1 then
keeper = keeper + 1
end
if j == 2 then
primeNumberCount = primeNumberCount - 1;
end
end
print('The latest prime number: ', number);
print('keeper ', keeper); -- usage
для сохранения бессмысленных проверок четных чисел на простоту (кроме 2) можно сделать нечто такое:
#include <stdio.h>
#include <stdlib.h>
#include <cstdint>
int main( int argc, char *argv[] )
{
uint16_t primeNumberCount = 100;
if (argc == 2) {
primeNumberCount = atoi(argv[1]);
}
uint32_t keeper = 0;
uint32_t number = 0;
uint32_t meanless = 0;
while (primeNumberCount > 0) {
++number;
uint16_t j = 0;
if (number % 2 == 1) {
for (uint32_t i = 1; i <= number; ++i) {
if (number % i == 0) {
++j;
}
}
} else {
for (uint32_t i = 1; i <= number; ++i) {
if (number % i == 0) {
++j;
} else {
++meanless;
}
}
}
if (j % 2 == 1) {
++keeper;
}
if (j == 2) {
--primeNumberCount;
}
}
printf("The latest prime number: %d\n", number);
printf("Keeper: %d\n", keeper); // usage
printf("Meow: %d\n", meanless); // usage
}
local primeNumberCount = 100
if #arg == 1 then
primeNumberCount = tonumber(arg[1])
end
local number = 0
local keeper = 0
local meanless = 0
while primeNumberCount > 0 do
number = number + 1
local j = 0
if number % 2 == 1 then
for i = 1, number do
if number % i == 0 then
j = j + 1
end
end
else
for i = 1, number do
if number % i == 0 then
j = j + 1
else
meanless = meanless + 1
end
end
end
if (j % 2) == 1 then
keeper = keeper + 1
end
if j == 2 then
primeNumberCount = primeNumberCount - 1
end
end
print('The latest prime number: ', number)
print('keeper: ', keeper) -- usage
print('Meow: ', meanless) -- usage
на скорость вычислений это особо не влияет, ибо нахождение остатка более тяжелая операция, чем сложение. времена выполнения у меня всё те же, прибавилось максимум мс 80-100 (2-3%)
вывод для С. У luajit время 19.5с
The latest prime number: 70657
Keeper: 265
Meow: 1247527516
real 0m9.723s
user 0m9.636s
sys 0m0.000s
если в таком случае на обоих процессорах luajit будет в 2 раза медленее С++, то да, luajit хорошо оптимизирует операции, чтобы не считать лишнего. если будет та же магия, то видимо это связано с оптимизациями для конкретного процессора.
P.S. на что способны предсказатели ветвлений я не знаю. если они умеют оптимизировать и такой странный код, то честь и хвала им.
Немного появилось свободного времени.
По поводу вашего последнего примера - AMD Ryzen 7 4800HS резулльтаты следующие:
c++ - 8.504s
luajit - 8.846s
Я несколько дней переписывал тест, так что бы исключить лишние вычисления для обоих ЯП. Перепробовал кучу вариантов оптимизаций, но все мимо. Luajit всегда был был быстрее C++.
Потом дело дошло до флагов оптимизации Luajit - http://luajit.org/running.html
Все что удалось получить на данный момент:
docker build -t benchmark . > /dev/null && \
docker run --rm benchmark g++ --version && echo '' && echo '' && \
docker run --rm --volume $(pwd):/app benchmark g++ -O0 /app/prime-number/c++/cmd.cpp -o /app/prime-number/c++/cmd.cpp_bin && \
docker run --rm --volume $(pwd):/app benchmark bash -c 'time /app/prime-number/c++/cmd.cpp_bin 7000'
g++ (Ubuntu 10.3.0-1ubuntu1) 10.3.0
Copyright (C) 2020 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
The latest prime number: 70657
real 0m8.467s
user 0m8.462s
sys 0m0.005s
docker build -t benchmark . > /dev/null && \
docker run --rm benchmark luajit -v && echo '' && echo '' && \
docker run --rm --volume $(pwd):/app benchmark bash -c 'time luajit -O3,-loop /app/prime-number/lua/cmd.lua 7000'
LuaJIT 2.1.0-beta3 -- Copyright (C) 2005-2017 Mike Pall. http://luajit.org/
The latest prime number: 70657
real 0m14.579s
user 0m14.572s
sys 0m0.000s
Основное влияние на производительнось дает этот флаг: loop
Более подробной инфы о том, как именно работает этот вид оптимизации я не нашел, все что удалось выловить - это общие фразы мол JIT может менять границы цикла для оптимизации.
А что именно, кстати, тестировалось? Вот Питон — это время работы бинарного файла, или время работы бинарного файла плюс компиляция, или же это вообще вместе с запуском CPython?
А файловый кэш прогревали?
А докер на каком образе собирали? Потому что образ Ubuntu и Alpine в некоторых тестах совсем разную производительность показывают.
Модикации минимальные:
1) код нужно оформить как отдельную функцию (это само по себе приносит прирост в 1.5 раза примерно и является хорошим стилем оформления кода)
2) применить декоратор njit
3) желательно использовать внутренний цикл while (опционально)
Время исполнения на i3-8100 (3.6 ГГц) — 2.6-2.7 с.
import time
from numba import njit
primeNumberCount = 5000
@njit
def find_primes(primeNumberCount):
number = 0
while (primeNumberCount > 0):
number += 1
j = 0
i = 0
while i <(number + 1):
i+=1
if (number % i == 0):
j += 1
if (j == 2):
primeNumberCount -= 1
return number
t0 = time.time()
number = find_primes(primeNumberCount)
print(time.time() - t0)
print ('The latest prime number: ', number)
Еще вопрос. У вас указан процессор i5-9400, для которого применяется турбобуст. Фиксировалась ли при тестировании частота?
Это что угодно, но не бенчмаркинг.
Судя по потому что пхп с джитом и без на числодробилке показал одинаковый результат, методика тестирования действительно хромает
- PHP 8.0 — 37.559s
- PHP 8.0 (JIT) — 30.414s
А должна быть примерно в 2 раза. Вот, например, результаты на моей машине этого же кода (добавил только таймер для получения времени).
Для 1000 итераций:
- PHP 8.0 — 0.62016010284424s
- PHP 8.0 JIT — 0.380774974823s
Для 7000 итераций (как у автора):
- PHP 8.0 — 53.366537094116s
- PHP 8.0 JIT — 29.954167842865s
$ lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Byte Order: Little Endian
Address sizes: 44 bits physical, 48 bits virtual
CPU(s): 16
On-line CPU(s) list: 0-15
Thread(s) per core: 2
Core(s) per socket: 8
Socket(s): 1
NUMA node(s): 1
Vendor ID: AuthenticAMD
CPU family: 23
Model: 96
Model name: AMD Ryzen 7 4800HS with Radeon Graphics
- php8 — 0m25.576s
- php8 (jit) — 0m8.511s
JIT: 0m8.558s
не JIT: 0m25.604s
1. зачем проверять четные числа на простоту? половину вычислений можно убрать
2. если число не делится ни на какое (кроме 1) до квадратного корня из этого числа, то число не будет делиться ни на что после (кроме самого числа). на больших числах это убирает огромное кол-во проверок.
3. зачем проверять деление всех чисел до корня, если составные числа среди оставшихся нечетных чисел встречаются довольно часто, а количество проверок при этом большое. можно каждые, например, 20 проверок делать ещё одну: если число уже составное, дальше не проверять.
с такими оптимизациями алгоритм для 5000 простых чисел выдает результат в 400 раз быстрее: luajit, 9.3s vs 0.020s; C, 4.5s vs 0.012s. Для 500000 С показывает 5.3s, luajit — 9.35s, неоптимизированные версии
для таких значений запускать не буду (она для С, чувствую, будет считать несколько дней, не говоря уже про вызов такого кода в режиме интерпретирования...)
gcc test.c -o test -Ofast -march=native -lm
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
int main( int argc, char *argv[] )
{
unsigned primeNumberCount = 100-2;
if (argc == 2) {
primeNumberCount = atoi(argv[1])-2;
}
unsigned number = 3;
unsigned count = 20;
while (primeNumberCount > 0) {
number+=2;
register unsigned j = 0;
unsigned i = 1;
unsigned max = floor(sqrt(number))+1;
while (i<max) {
unsigned num = ((max-i)<count ? (max-i) : ((max<count) ? max : count));
for (unsigned k = 0; k < num; ++i, ++k) {
if (number % i == 0) {
++j;
}
}
if (j>1) {
break;
}
}
if (j == 1) {
--primeNumberCount;
}
}
printf("The latest prime number: %u\n", number);
}
local primeNumberCount = 100-2 -- 2 и 3 не проверять
if #arg == 1 then
primeNumberCount = tonumber(arg[1])-2
end
local number = 3 -- 2 и 3 не проверять
local count = 20
while primeNumberCount > 0 do
number = number + 2 -- пропускать четные
local j = 0
local i = 1
local max = math.floor(math.sqrt(number))+1 -- исключить лишние проверки
while i<max do
local num = ((max-i)<count and (max-i) or ((max<count) and max or count))
for k = 1, num do
if number % i == 0 then
j = j + 1
end
i = i + 1
end
if j>1 then -- если уже понятно, что число составное, то перейти к следующему
break
end
end
if j == 1 then
primeNumberCount = primeNumberCount - 1
end
end
print('The latest prime number: ', number)
теперь можно считать и 3.000.000 первых простых чисел с помощью Lua (и luajit) за 20 секунд, а не 5.000 за 46с
ясное дело что статья писалась для сравнения порядков скорости разных ЯП, но такие комментарии лишь напоминание, что кроме выбора ЯП большую роль играют и оптимизации исходного кода
#include <math.h>
#include <iostream>
#include <vector>
int main( int argc, char *argv[] )
{
uint32_t primeNumberCount = 100-2;
if (argc == 2) {
primeNumberCount = atoi(argv[1])-2;
}
std::vector<uint32_t> primes{2,3};
primes.reserve(primeNumberCount);
uint32_t number = 3;
while (primeNumberCount > 0) {
number+=2;
const uint32_t max = floor(sqrt(number));
bool isPrime(true);
for (auto& i: primes) {
if (number % i == 0 ){
isPrime = false;
break;
}
if (i>max) {
break;
}
}
if (isPrime){
--primeNumberCount;
primes.push_back(number);
}
}
std::cout << "The latest prime number: " << number << std::endl;
}
local primeNumberCount = 100-2
if #arg == 1 then
primeNumberCount = tonumber(arg[1])-2
end
local number = 3
local count = 20
local primes = {2,3}
while primeNumberCount > 0 do
number = number + 2
local max = math.floor(math.sqrt(number))
local isPrime = true
for _,i in ipairs(primes) do
if number%i == 0 then
isPrime = false
break
end
if i>max then
break
end
end
if isPrime then
primeNumberCount = primeNumberCount - 1
table.insert(primes, number)
end
end
print('The latest prime number: ', number)
Но смысл данного кода не в том что бы он работал максимально быстр, а в том что бы его можно было максимально быстро воспроизвести на других ЯП.
Я конечно извиняюсь, но регистры использовать, это чит коды. К тестированию вообще вопросов нет. И так очевидно что каждый язык можно оптимизировать до бесконечности, и получать разные результаты. Автор описал логику одинаково кругом, да, можно было бы для каждого языка и улучшать бесконечно код. Но зачем? Автор сравнивает одинаковые с точки зрения рядового пользователя алгоритмы (последовательность операций), между разными языками, что делает результаты вполне сопоставимыми для сравнения.
простые синтетические тесты плохи тем, что они не показывают как язык/инструмент будет справляться с реальной задачей, а не с какой-то одноразовой (кому нужно вычислять простые числа больше одного раза?).
при этом сложные синтетические тесты требуют много времени на подготовку и как минимум среднюю квалификацию по каждому ЯП/инструменту, чтобы суметь его более-менее грамотно реализовать — это всё долго и дорого, но это лучше будет демонстрировать способность языков программирования решать широкий класс задач.
изначальный код автора демонстрирует только как разные ЯП справляются с сомнительной полезности однотипными вычислениями. ЯП умеют только это? много ли алгоритмов решения реальных задач требуют только такие вычисления и ничего более?
а вот оптимизация с мемоизацией затронет ещё и массивы, что для синтетического теста даст ему не столь печальный облик, хоть и всё ещё малорелевантный реальным использованиям ЯП.
ограничение по корню из максимума будет требовать вычислять этот корень — в физических движках и играх часто требуется вычислять корни, это добавит в сложность задачи взаимодействие со стандартной библиотекой и ffi
а про регистры: в этом случае можно компилировать и без этого ключевого слова (для этой задачи), тесты на этой задаче показали, что компилятор справляется и без моих подсказок ему. ну и: а почему это стандартные средства языка — чит код? в этой задаче все подходящие условия для использования этого функционала? значит надо использовать!
рядовые пользовательские алгоритмы — работа с массивами, кастомными структурами данных, парсинг строк, сортировки и фильтрации, сложные/полезные многоразовые мат вычисления.
я бы никаких нападок не делал на использованный метод автора (даже при большой неэффективности), если бы он, например, считал интеграл по методу Симпсона с каким-то мелким шагом; если бы был реализован алгоритм размытия гаусса и обработка двумерного массива (например, шум Перлина); или был бы реализован например такой алгоритм: массив заполняется ГПСЧ (одинаковым для всех языков), один проход по циклу фильтрует массив (создавая новый) из чисел в каком-то диапазоне значений из первого массива, второй проход цикла сортировал бы его и, например, выводил бы 20 наибольших (а-ля бэк магазина, HR или риелторского агенства).
это больше похоже на реальный код + затрагивает больше возможностей языка.
хотя конечно простенькие вычисления термодинамические, квантовохимические или квантовофизические были бы максимально похожи на реальное использование числодробительных алгоритмов, чем вот это вот всё, но кто их будет реализовывать…
n this test, the framework's ORM is used to fetch all rows from a database table containing an unknown number of Unix fortune cookie messages (the table has 12 rows, but the code cannot have foreknowledge of the table's size). An additional fortune cookie message is inserted into the list at runtime and then the list is sorted by the message text. Finally, the list is delivered to the client using a server-side HTML template. The message text must be considered untrusted and properly escaped and the UTF-8 fortune messages must be rendered properly.
Если вы можете хоть что-то из перечисленного воспроизвести для Assembler/NASM, то я очень сильно вам завидую. Ибо я не в состоянии даже убрать лишние нули в выводе числа для asm: 000070657
Да и основной посыл теста в том, что он одинаков для любого ЯП. А реализовать сложный тест на разных ЯП — не так просто.
Если код завернутый в функцию запустить через:
— pypy3 будет в 8 раз быстрее от исходного
— mypyc всего в 5 раз быстрее
— nuitka всего в 2
ПХПшники реально молодцы что смогли затащить jit в основную кодовую базу
Впрочем, спасибо за подсказки альтернатив, про mypyc почти не слышал до этого, про nuitka слышал в роли предшественника numba, нужно будет попробовать.
Бенчи точно меряют то, что заявлено?
Разница в работе программы написанной на С/C++ и Assembler/NASM в районе 15%
Это нормально. Современные компиляторы достаточно хороши в оптимизации, иногда обходя по качеству сгенерированных инструкций человека.
P.S. Почему в C++ тесте используется std::atoi
? В 17-м стандарте ввели std::from_chars
, который намного быстрее. Уже поддерживается реализациями stdlibc++ от MSVC (с 16-й версии) и GCC (с 12-й версии)
И судя по всему ноду спас хитрый компилятор либо что-то еще.
Чисто по приколу написал 2 простых файла на создание классов, запихивание их в массив и т.д.
И NodeJs оказался почти в 2 раза медленнее рубей, это магия…
На моем компе нода выдала результат за 5+ секунд, а руби уложились в 2.8
Если кому интересно, то вот эти два файла:
gist.github.com/Zig1375/a972207158edb523ce04219d84885469
PS:
NodeJS 12 (для работы нужна 12, по этому тестил на ней)
Ruby 3.0.1
«Дело было вечером, делать было нечего» или краткая история о сравнении производительности языков программирования