S0ER.Code
157 subscribers
3 links
Канал для обсуждения программирования
Download Telegram
Channel created
Forwarded from S0ER.live
В TypeScript 5.6 появилась более осознаная обработка всегда истинных выражений.

if (/0x[0-9a-f]/) {

}

Здесь забыли .test() после регулярки, но теперь это не проблема.

if (x => 0) {

}

Здесь перепутали >= и стрелочную функцию =>

function isValid(value: string | number, options: any, strictness: "strict" | "loose") { 

if (strictness === "loose") {
value = +value } return value < options.max ?? 100;
}

Здесь будет вот такой порядок: (value < options.max) ?? 100

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

error: This kind of expression is always truthy.
Forwarded from S0ER
Низкий уровень: как выглядят функции на ASM

Процессор умеет выполнять лишь простые машинные команды, как же тогда работают функции и классы языков высокого уровня?

Чтобы разобраться будем использовать Compiler Explorer который позволяет преобразовать конструкции высокого уровня в их представление на низком уровне (Assembler).

Начать предлагаю с того, что посмотреть какой код будет сгенерирован компилятором для следующего листинга:


int callme() {
return 1;
}

void main() {
callme();
}


в командной строке это можно сделать с помощью команды

gcc -g -o output.s -masm=intel -fno-verbose-asm -S -fdiagnostics-color=always example.c


но Compiler Expolrer делает это за нас, в результате получен следующий код:

callme:
push rbp
mov rbp, rsp
mov eax, 1
pop rbp
ret
main:
push rbp
mov rbp, rsp
mov eax, 0
call callme
nop
pop rbp
ret

Мы видим, что:

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

для вызова функции используется специальная инструкция call

для возврата из функции используется специальная инструкция ret

чтобы вернуть значение из функции используется регистр eax - mov eax,1
в функции есть специальные части "пролог" и "эпилог"

Что такое "Пролог"

Это часть функции которая сохраняет текущие значения регистров, чтобы восстановить их при возврате из функции.


push rbp; инструкция push сохраняет в стеке значение rbp

mov rbp, rsp; копирует значение регистра указателя вершины стека (открытие кадра стека)

sub rsp, xx; выделяем память под локальные переменные

1. rbp используется для адресации локальных переменных, должен быть сохранен в стеке;
2. rsp используется для указания на вершину стека

Что такое "эпилог"

Этр код, который закрывает кадр стека и восстанавливаем значние rpb
mov rsp, rbp
pop rbp
ret


Red zone

Вероятно вы заметили, что у нас в прологе нет инструкции sub rsp, xx, все дело в том, что у процессоров есть оптимизация, которая называется red zone, в данном случае - область размером 128 байт которая находится за пределами RSP и не должна изменяться обработчиками сигналов и прерываний.

В качестве индивидуального задания можете попробовать добавить char a[128]; в код функции callme и посмотреть что будет.


Вывод:
Сегодня мы узнали, что функции высокого уровня на уровне ассемблера размещаются в теле программы и доступ к ним осуществляется путем перехода по адресу, где находится соответствующая функция.

Часто узнать функции в коде на ассемблере можно по следующим признакам:

для вызова функций используются инструкции call, ret

без оптимизаций компилятор добавит специальные куски кода "пролог" и "эпилог"

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

#asm #знания

SOER | PRO | Boosty
Forwarded from S0ER
Решение проблемы избыточных состояний через конечные автоматы

Часто замечаю в коде у фронтенд-разработчиков такой паттерн, как выражение одного логического состояния через несколько состояний в коде. Рассмотрим, например, такой код на React, который описывает состояние некоторой формы:

const [error, setError] = useState<string | undefined>(); // в форме есть ошибки
const [loading, setLoading] = useState<boolean>(false); // форма загружается на сервер
const [isSuccessful, setIsSuccessful] = useState<boolean>(false); // форма успешно отправлена


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

1. в состоянии загрузки
2. в состоянии ошибки
3. в состоянии успеха
4. в состоянии готовности к редактированию.

Несложно посчитать, что код выше сгенерирует нам целых восемь различных состояний вместо четырех необходимых. Помимо запутанности и сложночитаемости, такой код плох тем, что мы генерируем недостижимые состояния, которые непонятно, как обрабатывать. Например, что делать, если у нас одновременно есть ошибка и переменная isSuccessful равна true? Скорее всего, программист выберет какое-то одно состояние как приоритетное, но так или иначе, такой код будет порождать запутанность и баги.

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

type FormState = {
state: 'loading';
} | {
state: 'error';
message: string;
} | {
state: 'ready';
} | {
state: 'successful';
}

type FormAction = {
type: 'set_loading'
} | {
type: 'set_error',
payload: {
error_message: string;
}
} | {
type: 'set_ready',
} | {
type: 'set_successful'
}

const initialState: FormState = { state: 'ready' };

const formReducer = (state: FormState, action: FormAction):FormState => {
// ...
}


Созданные formReducer и initialState мы можем использовать в нашем компоненте через useReducer. Таким образом, мы получили удобное декларативное описание возможных состояний нашей системы, а также совокупность переходов из одного состояния в другое.
🔥3
Forwarded from S0ER
У меня была попытка два года назад делать видео с аналитикой кодовых баз.
Я делал анализ репозитория NPM думаю, что интересно было бы возобновить подобную активность.

Что скажете?
👍3
Низкий уровень: как выглядят вызовы функций по указателю на ASM

В языках высокого уровня, таких как C или C++, часто используются указатели на функции. Это позволяет динамически выбирать, какую функцию вызвать в runtime. Но как это выглядит на уровне ассемблера? Давайте разберемся.

Для начала рассмотрим простой пример на C, где используется указатель на функцию:

int callme() { 
return 1;
}

void main() {
int (*func_ptr)() = callme;
func_ptr();
}

Здесь мы создаем указатель на функцию func_ptr, который указывает на функцию callme, и затем вызываем функцию через этот указатель.

Как это выглядит в ассемблере?

Используем Compiler Explorer, чтобы преобразовать этот код в ассемблер. Вот что получилось:

callme: 
push rbp
mov rbp, rsp
mov eax, 1
pop rbp
ret
main:
push rbp
mov rbp, rsp
sub rsp, 16
mov QWORD PTR [rbp-8], OFFSET FLAT:callme
mov rax, QWORD PTR [rbp-8]
call rax
nop
leave
ret

Что здесь происходит?

Создание указателя на функцию:

В функции main мы видим, что адрес функции callme сохраняется в памяти по адресу [rbp-8]:

mov QWORD PTR [rbp-8], OFFSET FLAT:callme

Здесь OFFSET FLAT:callme — это адрес функции callme в памяти.

Загрузка указателя в регистр:

Затем этот адрес загружается в регистр rax:

mov rax, QWORD PTR [rbp-8]

Вызов функции по указателю:

После этого происходит вызов функции через регистр rax:

call rax

Инструкция call использует значение в регистре rax как адрес функции, на которую нужно перейти.

Пролог и эпилог

Как и в случае с обычным вызовом функции, здесь также присутствуют пролог и эпилог:

Пролог:

push rbp
mov rbp,
rsp sub rsp, 16

Здесь сохраняется значение rbp, устанавливается новый кадр стека и выделяется место для локальных переменных.

Эпилог:

leave ret

Здесь восстанавливается значение rbp и выполняется возврат из функции.

Пример с массивом в функции

Давайте добавим массив в функцию callme и посмотрим, как это повлияет на ассемблерный код:

int callme() { 
char a[128];
return 1;
}

В ассемблере это будет выглядеть так:

callme:
push rbp
mov rbp, rsp
sub rsp, 8 ; <-- обратите внимание, тут сработала Red Zone
mov eax, 1
leave
ret

Здесь видно, что в прологе добавилась инструкция sub rsp, 8, которая выделяет место на стеке для массива a[128].

Вывод

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

Основные моменты:

👑 Указатель на функцию — это просто адрес функции в памяти.
Вызов функции по указателю осуществляется через регистр, в котором хранится адрес функции.

👑 Пролог и эпилог присутствуют как при обычном вызове функции, так и при вызове через указатель.

Таким образом, даже такие высокоуровневые конструкции, как указатели на функции, имеют свое прямое отражение в ассемблерном коде.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2😎1
Red Zone в x86-64

В архитектуре x86-64 существует концепция red zone — это область памяти размером 128 байт ниже указателя стека (RSP), которая может использоваться функциями без явного выделения места на стеке.

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

В нашем примере массив char a[128] как раз попадает в red zone, поэтому компилятор не выделяет дополнительное место на стеке с помощью sub rsp, 128. Вместо этого он просто использует red zone.
👍2🔥2
Всегда ли короткие имена переменных - зло?

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

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

Поэтому давайте сформулирую свои правила в отношении коротких имен переменных.

👑 Когда короткие имена приемлемы

В небольших методах с очевидным контекстом

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

Для итераторов в небольших циклах

В лямбда-функциях, где контекст ясен

Пример оправданного короткого имени.


function calculateDistance(x1: number, y1: number, x2: number, y2: number): number {
const dx = x2 - x1;
const dy = y2 - y1;
return Math.sqrt(dx * dx + dy * dy);
}

В этом случае:

x1, y1, x2, y2 - стандартные математические обозначения координат

dx, dy - общепринятые сокращения для "delta x" и "delta y"

А типы параметров (number) делают назначение переменных ещё понятнее

👑 Когда короткие имена недопустимы

Однако есть ситуации, когда короткие имена действительно становятся проблемой:

В больших методах, где контекст теряется

Для переменных с широкой областью видимости

Когда назначение переменной неочевидно

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

Пример плохого использования:


function processUserData(u: User, d: DataProcessor, c: Config) {
// ... 50 строк кода ...
if (u.s) { // Что такое 's'? Статус? Субscription? Счёт?
d.p(u); // Что делает 'p'? process? print? persist?
}
}

При этом вместо 'u' вполне можно было использовать сокращения usr, а вместо 'c' использовать cfg, это устоявшиеся сокращения, поэтому использовать их можно.

Когда есть возможность аннатировать переменную ее типом, с учетом небольшого размера функции, нормальным становится и вариант с u:


interface User {
id: string;
name: string;
age: number;
}

// Хорошо - тип ясен из контекста
function greet(u: User) {
console.log(`Hello, ${u.name}!`);
}


Для итераторов в небольших циклах короткие имена допустимы:



const numbers = [1, 2, 3];
// i - общепринятое имя для индекса
for (let i = 0; i < numbers.length; i++) {
console.log(numbers[i]);
}


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



const users = [{name: 'Alice'}, {name: 'Bob'}];
// n - понятно в контексте map
const names = users.map(u => u.name);


👑 Надеюсь, приведенные пример убедили вас, что в зависимости от ситуации короткие имена переменных не только допустимы, но и помогают писать выразительный код.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍6
TurboFan: анализ оптимизаций в V8 с помощью ассемблера (часть 1)

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

V8 — это движок для работы с языком JavaScript, он используется в NodeJS и браузере Chrome. Одна из особенностей этого движка — представление «горячего» кода в виде оптимизированных машинных инструкций. Для этого в движок интегрирован оптимизирующий JIT-компилятор TurboFan.

Основные задачи, которые решает TurboFan:

💡 Анализ и оптимизация «горячих» функций (которые вызываются часто);
💡 Специализация кода под конкретные типы данных;
💡 Удаление избыточных операций;
💡 Векторизация вычислений;
💡 Инлайнинг функций.

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

Основные моменты, которые нужны для старта:

1️⃣ Мы будем использовать nodejs для получения машинного кода функции, для этого нужно запомнить следующий шаблон команды:


node --print-opt-code --code-comments -allow-natives-syntax your_script.js


2️⃣Мы будем использовать специальную инструкцию %OptimizeFunctionOnNextCall, без нее ничего не получится.

Давайте сделаем простой скрипт:


// sum.js
function sum(a, b) {
return a + b;
}

// Прогреваем функцию (вызываем много раз, чтобы V8 её оптимизировал)
for (let i = 0; i < 10000; i++) {
sum(i, i + 1); // используем целые числа, чтобы TurboFan сделал оптимизацию именно под них
}

// Явно просим V8 оптимизировать функцию (требует --allow-natives-syntax) иначе в выводе не будет описане функции
%OptimizeFunctionOnNextCall(sum);
// Вызываем ещё раз (теперь с оптимизацией)
sum(1, 2);


Теперь запустим скрипт node --print-opt-code --code-comments -allow-natives-syntax sum.js, если мы сделали всё правильно, то получим огромный вывод на экран, из которого интересна вот эта часть:



--- Raw source ---
(a, b) {
return a + b;
}

--- Optimized code ---
optimization_id = 1
source_position = 22
kind = TURBOFAN
name = sum
stack_slots = 6
compiler = turbofan
address = 0x2edae09247a1

Instructions (size = 184)

; загрузка hidden класса объекта (проверка структуры)
0x1097064c0 0 488b59f8 REX.W movq rbx, [rcx-0x8]

; проверка контекста
0x1097064c4 4 f6433501 testb [rbx+0x35],0x1
0x1097064c8 8 0f85f2e03dfb jnz 0x104ae45c0 (CompileLazyDeoptimizedCode) ;; деоптимизация

; пролог
0x1097064ce e 55 push rbp
0x1097064cf f 4889e5 REX.W movq rbp, rsp
0x1097064d2 12 56 push rsi
0x1097064d3 13 57 push rdi
0x1097064d4 14 50 push rax

; выравнивание стека и проверка лимитов
0x1097064d5 15 4883ec08 REX.W subq rsp,0x8
0x1097064d9 19 488975e0 REX.W movq [rbp-0x20],rsi
0x1097064dd 1d 493b65a0 REX.W cmpq rsp, [r13-0x60] (external value (StackGuard::address_of_jslimit()))
0x1097064e1 21 0f8653000000 jna 0x10970653a <+0x7a> ; если стек переполнен, переходим

; ====== Основная функция ======

; Загрузка аргумента "a"
0x1097064e7 27 488b5518 REX.W movq rdx, [rbp+0x18]
0x1097064eb 2b f6c201 testb rdx,0x1
0x1097064ee 2e 0f8572000000 jnz 0x109706566 <+0xa6>

; Загрузка аргумента "b"
0x1097064f4 34 488b4d20 REX.W movq rcx, [rbp+0x20]
0x1097064f8 38 48c1f920 REX.W sarq rcx, 32 ; сразу распаковка SMI для "b"

; Подготовка аргументов к сложению
0x1097064fc 3c 488bfa REX.W movq rdi,rdx
0x1097064ff 3f 48c1ff20 REX.W sarq rdi, 32 ; распаковка для "a"
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1🤡1
; проверка типа второго аргумента "b"
0x109706503 43 4c8b4520 REX.W movq r8, [rbp+0x20]
0x109706507 47 41f6c001 testb r8,0x1
0x10970650b 4b 0f8559000000 jnz 0x10970656a <+0xaa>
; само сложение
0x109706511 51 03cf addl rcx,rdi

; если переполнение, то переход
0x109706513 53 0f8055000000 jo 0x10970656e <+0xae>

; упаковка результата
0x109706519 59 48c1e120 REX.W shlq rcx, 32

; результат кладем в rax
0x10970651d 5d 488bc1 REX.W movq rax,rcx

; ====== эпилог ==========
0x109706520 60 488b4de8 REX.W movq rcx, [rbp-0x18]
0x109706524 64 488be5 REX.W movq rsp,rbp
0x109706527 67 5d pop rbp

0x109706528 68 4883f903 REX.W cmpq rcx,0x3
0x10970652c 6c 7f03 jg 0x109706531 <+0x71>
0x10970652e 6e c21800 ret 0x18

0x109706531 71 415a pop r10
0x109706533 73 488d24cc REX.W leaq rsp, [rsp+rcx*8]
0x109706537 77 4152 push r10
0x109706539 79 c3 retl

0x10970653a 7a 48ba0000000010000000 REX.W movq rdx,0x1000000000
0x109706544 84 52 push rdx
0x109706545 85 48bb107d7a0401000000 REX.W movq rbx,0x1047a7d10
0x10970654f 8f b801000000 movl rax,0x1
0x109706554 94 48bef911d866da2e0000 REX.W movq rsi,0x2eda66d811f9 ;; object: 0x2eda66d811f9 <NativeContext[280]>
0x10970655e 9e e89db146fb call 0x104b71700 (CEntry_Return1_ArgvOnStack_NoBuiltinExit) ;; near builtin entry
0x109706563 a3 eb82 jmp 0x1097064e7 <+0x27>
0x109706565 a5 90 nop


Как видите, здесь довольно длинный ASM-кусок, в котором я добавил пояснения, чтобы было понятно по смыслу, что происходит, само сложение выполняется одной командой addl, всё остальное — это подготовки и обработка исключений, давайте коротко подведём итоги:

👑 сначала идёт пролог — это стандартная часть, которая сохраняет значения регистров;
👑 далее идёт загрузка аргументов функции «a» и «b»;
👑 проверка, что загруженные аргументы — это числа;
👑 далее идёт распаковка (обратите внимание, что на самом деле распаковка "b" сделана раньше, чем реальная проверка типа, это спекулятивная оптимизация, которая требует отдельного рассмотрения, как и само понятие «распаковка»);
👑 делаем вычисление;
👑 проверяем, что всё ок;
👑 упаковываем результат;
👑 финальные проверки и эпилог.

Более детально каждую из частей мы рассмотрим в будущих статьях, а пока ставь лайк, если тема тебе интересна. Полный вариант статьи
Please open Telegram to view this post
VIEW IN TELEGRAM
👍6🤡1