Как стать автором
Поиск
Написать публикацию
Обновить

Комментарии 20

Затем вычитаем 1 из n. Таким образом, n будет возрастать на каждом прогоне цикла.

Боюсь, что если из n вычитать 1, n будет скорее уменьшаться, чем возрастать...

Там еще один потенциальный косяк: функция факториала определена только для неотрицательных целых (к слову, 0!=1). Если в вызывающем коде этой проверки нет, то не исключено, что в функцию будет передано отрицательное целое. И функция в том виде, как написано вернет 1.

Си назван императивным по причине какой то обиды или скрытого пренебрежения?

Нет, это общее названии категории языков программирования. Никаких обид или пренебрежений, никаких психологических мотивов. См. хотя бы тут: https://ru.m.wikipedia.org/wiki/Императивное_программирование

Для посчитать факториал не нужен не С ни Ассемблер ни Хаскель. Вы вполне можете посчитать его в тетрадке столбиком и осчастливить человечество таблицей значений факториала навсегда. Это не программирование!

Программирование это когда вам надо посчитать квадрат числа каждую микросекунду, а у вас нет инструкции умножения в Ассемблере.

Вы вполне можете посчитать его в тетрадке столбиком и осчастливить человечество таблицей значений факториала навсегда.

Смелое утверждение :) Хотя факториал определен на множестве натуральных чисел и хотя это множество счетно (т.н.алеф-нуль), но это множество, увы, бесконечно. Вы же не пользуетесь таблицами Брадиса, чтобы узнать синус или логарифм? Я, по крайней мере лет 40 этого не делаю и другие, сдается мне, тоже. Так с какой стати кому-то нужны таблицы факториалов, тем более, что их полнота невозможна физически? Рано или поздно кому-то понадобится факториал, которого в такой таблице нет

Так с какой стати кому-то нужны таблицы факториалов, тем более, что их полнота невозможна физически?

А вы не видите предела до которого приведенная функция будет работать на х86 архитектуре? Достаточно таблицы на 15 максимум до 20 значений!

Это так, но, ещё раз — это же просто простая демка к командам ассемблера, к примеру, написать на чистом асме функцию, которая посчитает и напечатает все цифры, скажем 1000! — тоже хорошее упражнение. На самом деле такие функции в продакшене или при реальных расчётах не пишет никто — есть же библиотеки, скажем в Матлабе я просто возьму Symbolic Math Toolbox, и там вроде вообще лимита нет (за исключением памяти компа).

Это же учебный пример. Ещё Фибоначчи берут иногда. Из таких вот маленьких кирпичиков как раз и складывается программирование. А Хаскель тут слегка избыточен, я согласен, просто автор оригинала (это перевод) судя по всему хорошо им владеет, а вот Си и Ассемблером чуть менее уверенно, судя по терминологии и опущенным важным деталям о механизме возврата из функции.

Соглашусь! И добавлю: нахождение НОД по методу Евклида тоже часто рассматривается как пример алгоритма

Мне, в конце 80-х, тоже тоже давали учебный пример про факториал посчитать, и вывести число в десятичном виде неограниченной длины (тоже конечно условно неограниченного, но с пределом который не так просто определить, хотя бы). Эта задача решается на компьютере, но тем же столбиком, как ни странно! И это по моему действительно погружает в проблемы программирования, в понимание применения базовых элементов для понимания программирования, таких как массивы, циклы, чем отличается машинное представление чисел от десятичного человеческого... А самое интересное, этот пример демонстрирует что ручные методы "в лоб" при использовании компьютера, зачастую, оказываются самыми эффективными.

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

самый топ по обучению это плотная работа с текстом(редактирование, вставка, взятие текста, открыть/закрыть файл, и там число дробилок не меньше кстати ))

например текстовый буффер(сама структура его функции, и вторая его часть его рисование )

при желании углубления обучения, можно углубиться в формат текста(ttf например, тут можно познакомиться зачем нужна математика в текстовом буффере в полной мере)

просто факториал или 1 алгоритм это часть от стандарта, который предоставляет точность расчета, по началу это не интересно и не понятно зачем надо, а с текстом всё нагляднее

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

Во-первых буфер а не буфер. Во-вторых ttf это не формат текста, а формат описания шрифта от Эппл и Майкрософт. В-третьих факториал это математическая функция, а никакой не стандарт.

Прежде чем учить, сначала сами разберитесь во всем.

хорошо, спасибо

Для обучения ассемблера, кстати, могу смело порекомендовать Евро Ассемблер (euroassembler.eu). Это на самом деле офигенная, хотя и мало известная игрушка, причём написанная одним человеком из Чехии, который охотно отвечает на вопросы. Что удобно, это ассемблер и компоновщик в одном флаконе, написанный на себе самом.

Если хочется сделать минимальное консольное приложение под Windows для вычисления факториала, то кода будет всего ничего:

EUROASM AutoSegment=Yes, CPU=X64
fact PROGRAM Format=PE, Width=64, Model=Flat, ListMap=Yes, IconFile=, Entry=Start:

INCLUDE winscon.htm, winabi.htm, cpuext64.htm

Msg1 D "Enter Number:",0
Msg2 D "Factorial is ",0
Buffer DB 80 * B
    
Start: nop
	StdOutput Msg1, Console=Yes
	StdInput Buffer ; Input Number
	LodD Buffer ; https://euroassembler.eu/maclib/cpuext64.htm#LodD (rax <- Num)

	mov rbx, 1        ; Initialize result to 1
.loop:
	imul rbx, rax     ; Multiply rbx by rax
	dec rax           ; Decrement rax
	cmp rax, 1        ; Check if rax <= 1
	jg .loop          ; If rax > 1, repeat loop
	mov rax, rbx      ; Move result to rax

	StoD Buffer ; https://euroassembler.eu/maclib/cpuext64.htm#StoD (from rax)
	StdOutput Msg2, Buffer, Console=Yes
	TerminateProgram

ENDPROGRAM fact

Я код чутка поправил, так как два перехода тут не нужны совершенно.

Ассемблируется командой

euroasm.exe fact.asm
Походу генерит листинг, в котором все ходы расписаны
|                          |EUROASM AutoSegment=Yes, CPU=X64
|                          |fact PROGRAM Format=PE, Width=64, Model=Flat, ListMap=Yes, IconFile=, Entry=Start:
|[.text]                   ::::Section changed.
|00000000:                 |
|00000000:                 |INCLUDE winscon.htm, winabi.htm, cpuext64.htm
|00000000:                 * INCLUDE "./maclib/winscon.htm"
|[.text]                   ::::Section changed.
|00000000:                 * INCLUDE "./maclib/winabi.htm"
|00000000:                 * INCLUDE "./maclib/cpuext64.htm"
|00000000:                 |
|[.data]                   ::::Section changed.
|00000000:456E746572204E75~|Msg1 D "Enter Number:",0
|0000000E:466163746F726961~|Msg2 D "Factorial is ",0
|[.bss]                    ::::Section changed.
|00000000:................~|Buffer DB 80 * B
|00000050:                 |
|[.text]                   ::::Section changed.
|00000000:90               |Start: nop
|00000001:                 |	StdOutput Msg1, Console=Yes
|00000018:                 |	StdInput Buffer ; Input Number
|0000002F:                 |	LodD Buffer ; https://euroassembler.eu/maclib/cpuext64.htm#LodD (rax <- Num)
|0000003B:                 |
|0000003B:BB01000000       |	mov rbx, 1        ; Initialize result to 1
|00000040:                 |.loop:
|00000040:480FAFD8         |	imul rbx, rax     ; Multiply rbx by rax
|00000044:48FFC8           |	dec rax           ; Decrement rax
|00000047:4883F801         |	cmp rax, 1        ; Check if rax <= 1
|0000004B:7FF3             |	jg .loop          ; If rax > 1, repeat loop
|0000004D:4889D8           |	mov rax, rbx      ; Move result to rax
|00000050:                 |
|00000050:                 |	StoD Buffer ; https://euroassembler.eu/maclib/cpuext64.htm#StoD
|0000005E:                 |	StdOutput Msg2, Console=Yes
|00000075:                 |	StdOutput Buffer, Console=Yes
|0000008C:                 |	TerminateProgram
|0000009A:                 |
|                          |ENDPROGRAM fact
|        **** ListMap "fact.exe",model=FLAT,groups=0,segments=5,entry=Start:,stack=
|          [.text],FA=00000400h,VA=00401000h,size=00000479h=1145,width=64,align=0010h,purpose=CODE+LITERAL
|          [.data],FA=00000A00h,VA=00402000h,size=0000001Ch=28,width=64,align=0010h,purpose=DATA
|          [.bss],FA=00000C00h,VA=00403000h,size=00000050h=80,width=64,align=0010h,purpose=BSS
|          [.idata],FA=00000C00h,VA=00404000h,size=00000173h=371,width=64,align=8,purpose=IMPORT+IAT
|          [.reloc],FA=00000E00h,VA=00405000h,size=00000030h=48,width=32,align=4,purpose=BASERELOC
|        **** ListGlobals "fact.exe",Global=0,Public=3,Extern=0,eXport=0,Import=8
|        ExitProcess,[.idata]:0000016Ch,VA=0040416Ch,scope='I',lib="kernel32.dll"
|        GetStdHandle,[.idata]:00000150h,VA=00404150h,scope='I',lib="kernel32.dll"
|        LodD64@RT,[.text]:0000023Eh,VA=0040123Eh,scope='P'
|        ReadConsoleA,[.idata]:00000165h,VA=00404165h,scope='I',lib="kernel32.dll"
|        ReadConsoleW,[.idata]:0000015Eh,VA=0040415Eh,scope='I',lib="kernel32.dll"
|        ReadFile,[.idata]:00000157h,VA=00404157h,scope='I',lib="kernel32.dll"
|        Start,[.text]:00000000h,VA=00401000h,scope='P'
|        StoD64@RT,[.text]:000002CDh,VA=004012CDh,scope='P'
|        WriteConsoleA,[.idata]:00000142h,VA=00404142h,scope='I',lib="kernel32.dll"
|        WriteConsoleW,[.idata]:00000149h,VA=00404149h,scope='I',lib="kernel32.dll"
|        WriteFile,[.idata]:0000013Bh,VA=0040413Bh,scope='I',lib="kernel32.dll"

На выходе исполняемый файл в три с половиной килобайта и работает:

>fact.exe
Enter Number:6
Factorial is 720

Умеет в AVX512, можно сделать DLL вместо консольного приложения. Использую время от времени для небольших набросков и тонкой настройки и бенчмаркинга узких мест в коде (после интрисиков). Эх, надо будет статью как-нибудь написать.

Из современных книжек могу порекомендовать Даниэль Куссвюрм — Профессиональное программирование на ассемблере x64 с расширениями AVX, AVX2 и AVX-512:

Либо, лучше последнее издание на английском. Она не идеальна, но основы там даны про регистры, стек и всё такое.

Я код чутка поправил, так как два перехода тут не нужны совершенно.

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

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

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

EUROASM AutoSegment=Yes, CPU=X64
benchm PROGRAM Format=PE, Width=64, Model=Flat, ListMap=Yes, IconFile=, Entry=Start:

INCLUDE winscon.htm, winabi.htm, cpuext64.htm

Msg0 D "Two Loops Benchmark",13,10,0
Msg1 D "Single Branch Ticks:  ",0
Msg2 D "; Dual Branch Ticks:  ",0
Buffer DB 80 * B
N DQ 1_000_000_000 ; Amount of Iterations

StartBench %MACRO
	cpuid ; force all previous instructions to complete (rax...rdx reset)
	rdtsc ; read time stamp counter
	mov edi, eax ; save EAX for later
	mov esi, edx ; save EDX for later
%ENDMACRO StartBench

EndBench %MACRO
	cpuid ; wait to complete before RDTSC
	rdtsc ; read time stamp counter again
	sub eax, edi ; subtract the most recent CPU ticks from the original CPU ticks
	sbb edx, esi ; now, subtract with borrow
	shl rax, 32
	shrd rax, rdx, 32 ; ticks are in rax
%ENDMACRO EndBench
    
Start: nop
	StdOutput Msg0, Console=Yes

; ======== Single Branch Test ======== 
	StartBench 
	mov rbx, 1        ; Initialize result to 1
	mov rax, [N]	  ; Amount of interations
.loop1:
	imul rbx, rax     ; Multiply rbx by rax
	dec rax           ; Decrement rax
	cmp rax, 1        ; Check if rax <= 1
	jg .loop1         ; If rax > 1, repeat loop
	EndBench
	StoD Buffer
	StdOutput Msg1, Buffer, Console=Yes

; ======== Dual Branch Test ======== 
	StartBench
    mov rbx, 1
	mov rax, [N]
.loop2:
	cmp rax, 1
	jle .done
	imul rbx, rax
	dec rax
	jmp .loop2
.done:
	EndBench
	StoD Buffer
	StdOutput Msg2, Buffer, Console=Yes

	TerminateProgram

ENDPROGRAM benchm

Результат примерно такой:

>benchm.exe
Two Loops Benchmark
Single Branch Ticks:  1914967186; Dual Branch Ticks:  1894910657

В общем да, небольшая разница есть, но почти гомеопатическая (если прогоны местами поменять, то тенденция та же, я проверил).

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

	mov rbx, 1        ; Initialize result to 1
.loop1:
	imul rbx, rax     ; Multiply rbx by rax
	dec rax           ; Decrement rax
	jnz .loop1        ; If rax > 1, repeat loop

	mov rax, rbx      ; Move result to rax

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

Спасибо за проверку!

Думаю, что в 2025 году учить систему команд Intel – пустая трата времени. Притом, что она сама по себе очень громоздкая и запутанная.

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

sin(x) = cos (π/2-x)

Само по себе математическое определение не подразумевает никаких вычислений или даже их потенциальной возможности.

На этапе рассуждений о Хаскеле автор обходит это обстоятельство, незаметно подменяя определение функции

factorial n = product [1..n]

её применением

factorial 5

Либо эту статью написал LLM, либо автор находится на его уровне разумности.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации