Společnost Valve publikovala přehled To nej roku 2024 ve službě Steam aneb ohlédnutí nejen za nejprodávanějšími, nejprodávanějšími nově vydanými a nejhranějšími herními tituly roku 2024.
Byla vydána verze 3.4.0, a hned na to verze 3.4.1 řešící pouze číslo verze, programovacího jazyka Ruby (Wikipedie). Podrobný přehled novinek a změn v souboru NEWS na GitHubu. Ruby lze vyzkoušet na webové stránce TryRuby.
Všem čtenářkám a čtenářům AbcLinuxu krásné Vánoce.
Byla vydána nová verze 2.9.0 svobodného softwaru ScummVM (Wikipedie) umožňujícího bezproblémový běh mnoha klasických adventur na zařízeních, pro které nebyly nikdy určeny. Přehled novinek v poznámkách k vydání.
Byl vydán Sublime Text 4 Build 4189. Sublime Text (Wikipedie) je proprietární multiplatformní editor textových souborů a zdrojových kódů. Ke stažení a k vyzkoušení je zdarma. Pro další používání je nutná licence v ceně 99 dolarů. Spolu se Sublime Merge je cena 168 dolarů.
Vývojáři postmarketOS vydali verzi 24.12 tohoto před sedmi lety představeného operačního systému pro chytré telefony vycházejícího z optimalizovaného a nakonfigurovaného Alpine Linuxu s vlastními balíčky. Přehled novinek v příspěvku na blogu. Na výběr jsou 4 uživatelská rozhraní: GNOME Shell on Mobile, KDE Plasma Mobile, Phosh a Sxmo.
Byla vydána nová verze 9.9 z Debianu vycházející linuxové distribuce DietPi pro (nejenom) jednodeskové počítače. Přehled novinek v poznámkách k vydání.
Jonathan Thomas oznámil vydání nové verze 3.3.0 video editoru OpenShot (Wikipedie). Představení novinek také v písničce na YouTube. Zdrojové kódy OpenShotu jsou k dispozici na GitHubu. Ke stažení je i balíček ve formátu AppImage. Stačí jej stáhnout, nastavit právo na spouštění a spustit.
Byla vydána nová verze 5.0.0 programu na úpravu digitálních fotografií darktable (Wikipedie).
Před 50 lety, v prosinci 1974, byl představen počítač Altair 8800 od společnosti MITS (Micro Instrumentation and Telemetry Systems). Založen byl na procesoru Intel 8080. Prodával se jako stavebnice za 439 dolarů. V dnešních cenách by to bylo přes 2500 dolarů. Článek o Altair 8800 byl publikován v lednového čísle, na stáncích bylo už v prosinci, časopisu Popular Electronics (pdf). Videoukázky práce s počítačem Altair 8800 na YouTube: Altair 8800 Instructional Videos.
Tento blog byl smazán autorem
Tiskni Sdílej:
V dnešním zápisku bych chtěl představit pokročilé možnosti generování kódu pomocí knihovny AsmJit. V průběhu práce na grafické knihovně BlitJit jsem došel k závěru, že psát kód na úrovni assembleru je velmi komplikované a při skládání více kousků kódu dohromady může být vytváření výsledného kódu opravdu peklo pro programátora. Abych sobě i ostatním ulehčil život a zrychlil vývoj aplikací (a knihoven) využívajících AsmJit, vytvořil jsem třídu Compiler, která se snaží minimalizovat úsilí věnované na lepení existujících částí kódu a samozřejmě má za cíl ulehčit i vývoj těchto částí.
První verze knihovny AsmJit obsahovala pouze generátor kódu pro 32bitový x86 procesor. Jednalo se hlavně o jeden velký hlavičkový soubor, který byl založený na kódu z V8 (Google V8). Průběh generování assembleru probíhal tak, že se pomocí metod v hlavní třídě X86 (nyní se jmenuje Assembler) přímo emitoval kód do bufferu, který byl následně překopírován do paměti, kde mohl být kód spuštěn. Mezi největší nevýhody patřilo to, že se dal generovat pouze kód pro 32bitový procesor.
V průběhu vývoje jsem přidal podporu pro 64 bitů a pár věcí, o které mě požádali ostatní (byl to přepis kódu na konkrétní pozici a relokace). V této fázi jsem byl s už knihovnou celkem spokojený a chtěl jsem si vyzkoušet, jak používá v praxi (tedy napsat něco, co tuto knihovnu využívá). Zkusil jsem tedy začal psát knihovnu BlitJit. Už při psaní prvních funkcí jsem zjistil, že současné možnosti mi nestačí, a že by se mi hodilo používat mnohem vyšší konstrukce – začala práce na třídě Compiler a kompletní reorganizace knihovny AsmJit.
(toto můžete přeskočit, není to důležité)
Design knihovny AsmJit před reorganizací vypadal nějak takto (organizace tříd):
X86 – hlavní třída pro generování kódu Op – Operand (Register, Memory, Immediate) Register, MMRegister, XMMRegister – registry byly zvláštní třída, fungovaly ale jako Operand. Label – návěští
jednalo se prakticky o kód z Google V8, prototypy instrukcí vypadaly nějak takto:
void X86::mov(const Op& dst, const Op& src) { ASMJIT_ASSERT() – kontrola vstupů ... – kód pro emitování instrukce }
Tento design měl 2 velké nevýhody. Mnoho instrukcí si kontrolovalo správnost vstupu až za běhu (tedy ne při kompilaci v C++) a emitování kódu bylo prováděno přímo z funkcí ve třídě X86.
Rozhodl jsem se, že upustím celý koncept assembleru z V8 a navrhnu vlastní řešení, které bude sice kompatibilní, ale bude umožňovat mnohem více. Prakticky jsem zahodil celý kód a začal jsem psát všechno znovu.
Změny v bodech:
Rozepisovat reimplementaci asi nemá cenu, důležitý je ale fakt, že nová knihovna je z 99% zpětně kompatibilní a umožňuje toho mnohem víc.
Organizace tříd v AsmJit:
Operand:: (operandy) - BaseRegMem:: - BaseReg:: - Register, - X87Register, - MMRegister, - XMMRegister, - Mem, - Immediate Serializer:: ( obsahuje asm instrukce (x86, x64), všechny instrukce jsou serializováné do jediné pure virtual metody, která je implementovaná v Assembler a Compiler ) - Assembler – umožňuje generovat x86/x64 kód. - Compiler – umožňuje vytvářet funkce, obsahuje alokátor registrů a uchovávání stavů mezi bloky kódu. Label - návěští
Vzniklo ještě celkem dost tříd, které se používají spolu s třídou Compiler, budou představeny v příkladech.
Podle mě je nyní návrh robustní. Knihovna AsmJit se rozpadla na méně částí, které jsou na mnohem lepší úrovni než dříve. Například generování strojového kódu zajišťuje třída Assembler, Serializaci (intrinsics) zajišťuje Serializer a o vyšší konstrukce se stará Compiler a jeho pomocné třídy.
Mezi nejdůležitější novinku patří třída Compiler, o které měl být původně celý tento zápisek (trochu jsem rozepsal ty změny... no:) ). Umožňuje totiž vytvářet funkce pro 32bitové i 64bitové procesory (x86/x64) bez změn v kódu.
Compiler vám pomůže z:
Jako první bych chtěl začít definicí funkce, protože funkce bude asi vždycky základ toho, co budeme tvořit. Compiler obsahuje metody newFunction() a EndFunction(), které si představíme na následujícím kódu.
using namespace AsmJit; // Vytvoření instance Compiler Compiler c; // Vytvoření funkce prototypu void (*Fn)( int * x, int * y); Function& f = * c.newFunction( CALL_CONV_DEFAULT, // calling convention BuildFunction2<int *, int *>() // prototyp funkce pomocí šablon ); ... váš kód // Konec funkce c.endFunction();
Myslím, že kód je celkem jasný, ale i přesto bych chtěl ujasnit pár věcí. Všechno, co vytvoří třída Compiler tak taky v budoucnu uvolní. Není tedy možné si ponechat nějakou instanci vytvořenou pomocí třídy Compiler a pracovat s ní po jeho zániku. To je i důvod, proč Compiler vždy vrací návratové hodnoty jako ukazatele. Další věc je ta, že k vytvoření prototypu funkce slouží šablony BuildFunctionX<>. Díky šablonám se nemůže stát, že by jste se spletli v typu nebo by Compiler váš typ špatně rozpoznal. Identifikaci typu je zjištěná už při kompilaci C++ kódu.
Ve funkci budeme chtít určitě využít argumenty funkce a proměnné. Mezi argumentem a proměnnou nedělá Compiler rozdíl. Argument má tu vlastnost navíc, že je ihned alokován buď v registru nebo na zásobníku (podle volací konvence, typu argumentu a pořadí). Samozřejmě si můžeme argument, který byl předán na zásobníku alokovat do registru a naopak.
Následující kód využije argumenty funkce:
using namespace AsmJit; // Vytvoření instance Compiler Compiler c; // Vytvoření funkce prototypu void (*Fn)(int* x, int * y); Function& f = * c.newFunction( CALL_CONV_DEFAULT, // calling convention BuildFunction2<int *, int *>() // prototyp funkce pomocí šablon ); // Argumenty // f.argument(X) vrací Variable*, které vždy obalujeme pomocí tříd, // které mají sufix Ref (PtrRef, Int32Ref, Int64Ref, MMRef, XMMRef, ...) PtrRef a1(f.argument(0)); PtrRef a2(f.argument(1)); // a do ukazatelů můžeme uložit nějaké hodnoty c.mov( dword_ptr(a1.r()), imm(10) ); c.mov( dword_ptr(a2.r()), imm(20) ); // Konec funkce c.endFunction();
Takto vytvořená funkce uloží do argumentů funkce x a y hodnoty 10 a 20. Jedná se o celkem primitivní příklad, na kterém bych chtěl ukázat právě to, že se nestaráme o registry. Tím, že jsme použili třídu PtrRef a zavolali metodu PtrRef::r() jsme alokovali proměnnou do registru. Na platformě x86 může vypadat vygenerovaný assembler takto:
L1: push ebp mov ebp, esp L2: mov ecx, [ebp + 8] mov [ecx], 0xA mov edx, [ebp + 12] mov [edx], 0x14 L3: mov esp, ebp pop ebp ret
Můžeme si všimnout, že Compiler vygeneroval prolog, tělo funkce a epilog. Protože prolog a epilog může být někdy zbytečný, můžeme tuto volbu vypnout (ale jen na vlastní riziko;) ). Pokud zavoláme Function::setNaked(true), vypneme generování prologu a epilogu funkce a Compiler vytvoří následující kód:
L1: L2: mov ecx, [esp + 4] mov [ecx], 0xA mov edx, [esp + 8] mov [edx], 0x14 L3: ret
Možností, jak ovlivnit výsledný kód je více. Mezi nejčastější patří alokování některých registrů hned při vstupu do funkce, to můžeme udělat zavoláním VariableRef::alloc() (nebo v našem případě PtrRef::alloc). Ukázka:
using namespace AsmJit; // Vytvoření instance Compiler Compiler c; // Vytvoření funkce prototypu void (*Fn)(int* x, int * y); Function& f = * c.newFunction( CALL_CONV_DEFAULT, // calling convention BuildFunction2<int *, int *>() // prototyp funkce pomocí šablon ); // Nepoužívat prolog / epilog f.setNaked(true): // Argumenty // f.argument(X) vrací Variable*, které vždy obalujeme pomocí tříd, // které mají suffix Ref (PtrRef, Int32Ref, Int64Ref, MMRef, XMMRef, ...) PtrRef a1(f.argument(0)); PtrRef a2(f.argument(1)); // alokujeme si registry hned na začátku a1.alloc(); a2.alloc(); // a do ukazatelů můžeme uložit nějaké hodnoty c.mov( dword_ptr(a1.r()), imm(10) ); c.mov( dword_ptr(a2.r()), imm(20) ); // Konec funkce c.endFunction();
Vygenerovaný kód:
L1: L2: mov ecx, [esp + 4] mov edx, [esp + 8] mov [ecx], 0xA mov [edx], 0x14 L3: ret
Výsledný kód už je optimální a žádný C překladač v našem případě neudělá lepší (na tak malou funkci je to samozřejmě pochopitelné). Ale jedná se o tak primitivní funkci, kterou by jsme v praxi asi nikdy nekompilovali just-in-time. Nyní bych se chtěl přesunout k vytváření vlastních proměnných. Ty se vytvářejí pomocí metody Compiler::newVariable(typ, priorita), kde typ může nabývat těchto hodnot:
Vytvořenou proměnnou opět obalujeme například do PtrRef, Int32Ref, SysIntRef, atd... Když už jsme u proměnných, možná bych mohl vysvětlit, proč se obalují do referenčních typů. Proměnné jsou založené na metodě reference counting, obalení proměnné například do PtrRef znamená zvýšení počtu referencí a destruktor PtrRef zajistí snížení reference. Pokud reference klesne na 0, znamená to, že proměnná ukončila svůj život v současném bloku a Compiler ji může využít později. Zánik také znamená, že se její poslední obsah už nebude ukládat zpátky na zásobník (pro Compiler je v danou chvíli mrtvá). Priorita proměnné slouží alokátoru registrů pro posouzení, kterou proměnnou uložit na zásobník (spill) v případě, že je potřeba alokovat registr a všechny jsou již alokované. Čím nižší priorita, tím větší šance, že nedojde k dealokaci (spill). Pokud je priorita 0, tak alokátor nikdy nesmí danou proměnnou uložit na zásobník (a proměnná nemá na zásobníku ani vyhrazené místo).
V dalším příkladě bych chtěl ukázat, co se stane, když potřebujeme více proměnných než registrů. V tomto případě totiž alokátor musí některé proměnné přesouvat z registru na zásobník a naopak (v tomto případě už by překladač C/C++ měl generovat lepší kód).
// Complete compiler example #include <stdio.h> #include <stdlib.h> #include <string.h> #include <AsmJit/AsmJitAssembler.h> #include <AsmJit/AsmJitCompiler.h> #include <AsmJit/AsmJitVM.h> #include <AsmJit/AsmJitPrettyPrinter.h> // This is type of function we will generate typedef void (*MyFn)(int*, int*); int main(int argc, char* argv[]) { using namespace AsmJit; // ========================================================================== // STEP 1: Create function. Assembler a; // Log assembler output PrettyPrinter logger; a.setLogger(&logger); // Use compiler to make a function { Compiler c; Function& f = *c.newFunction(CALL_CONV_DEFAULT, BuildFunction2<int*, int*>()); // Possibilities to improve code: // f.setNaked(true); // f.setAllocableEbp(true); PtrRef a1(f.argument(0)); PtrRef a2(f.argument(1)); // Create some variables, default variable priority is 10. Int32Ref x1(f.newVariable(VARIABLE_TYPE_INT32)); Int32Ref x2(f.newVariable(VARIABLE_TYPE_INT32)); Int32Ref x3(f.newVariable(VARIABLE_TYPE_INT32)); Int32Ref x4(f.newVariable(VARIABLE_TYPE_INT32)); Int32Ref x5(f.newVariable(VARIABLE_TYPE_INT32)); Int32Ref x6(f.newVariable(VARIABLE_TYPE_INT32)); Int32Ref x7(f.newVariable(VARIABLE_TYPE_INT32)); Int32Ref x8(f.newVariable(VARIABLE_TYPE_INT32)); // Set our variables (use mov with reg/imm to se if register // allocator works) // x() means that there will be not read operation, only write c.mov(x1.x(), 1); c.mov(x2.x(), 2); c.mov(x3.x(), 3); c.mov(x4.x(), 4); c.mov(x5.x(), 5); c.mov(x6.x(), 6); c.mov(x7.x(), 7); c.mov(x8.x(), 8); // Create temporary variable Int32Ref t(f.newVariable(VARIABLE_TYPE_INT32)); // Set priority to 5 (lower probability to spill) t.setPriority(5); // Make sum (addition) // r() means that we can read or write to register, c() means that register // is used only for read (compiler can use informations collected by these // functions to optimize register allocation and spilling). // // If you are not sure about .c(), .x() and .r(), use always .r(). c.xor_(t.r(), t.r()); c.add(t.r(), x1.c()); c.add(t.r(), x2.c()); c.add(t.r(), x3.c()); c.add(t.r(), x4.c()); c.add(t.r(), x5.c()); c.add(t.r(), x6.c()); c.add(t.r(), x7.c()); c.add(t.r(), x8.c()); // Store result to a given pointer in first argument c.mov(dword_ptr(a1.c()), t.c()); // Make sum (subtraction) c.xor_(t.r(), t.r()); c.sub(t.r(), x1.c()); c.sub(t.r(), x2.c()); c.sub(t.r(), x3.c()); c.sub(t.r(), x4.c()); c.sub(t.r(), x5.c()); c.sub(t.r(), x6.c()); c.sub(t.r(), x7.c()); c.sub(t.r(), x8.c()); // Store result to a given pointer in second argument c.mov(dword_ptr(a2.c()), t.c()); // Finish c.endFunction(); c.build(a); } // ========================================================================== // ========================================================================== // STEP 2: Alloc execution-enabled memory SysUInt vsize; void *vmem = VM::alloc(a.codeSize(), &vsize, true); if (!vmem) { printf("AsmJit::VM::alloc() - Failed to allocate execution-enabled memory.\n"); return 1; } // Relocate generated code to vmem. a.relocCode(vmem); // Cast vmem to our function and call the code. int x; int y; function_cast<MyFn>(vmem)(&x, &y); printf("\nResults from JIT function: %d %d\n", x, y); // Memory should be freed, but use VM::free() to do that. VM::free(vmem, vsize); // ========================================================================== return 0; }
Tento příklad je už kompletní C++ program, který je součástí knihovny AsmJit (testcompiler.cpp). Jsou v něm použité i věci, o kterých jsem zatím nepsal. Pro logování výsledného kódu je hned na začátku použitá třída PrettyPrinter. Jedná se o volitelnou komponentu, která loguje výsledný kód do stderr (lze samozřejmě změnit). Lze taky vidět, že Compiler používá Assembler k serializaci výsledného kódu (Compiler totiž taky patří mezi volitelné komponenty). Místo VariableRef::r() se v kódu používá VariableRef::x() a VariableRef::c(). Jedná se jen o pomoc pro alokátor registrů, protože pokud použijeme VariableRef::r(), tak mu řekneme, že proměnnou chceme změnit a on ji při dealokaci uloží na zásobník. Pokud místo VariableRef::r() použijeme VariableRef::c(), tak říkáme, že chceme registr jen pro čtení (nemodifikujeme) a VariableRef::x() znamená, že chceme registr jen pro přepis (v takovém případě zase při alokaci proměnné do registru nenačítáme původní hodnotu).
Druhá část kódu alokuje virtuální paměť a spustí vytvořenou funkci. Výsledek je v našem případě 36 a -36.
Vygenerovaný kód:
L1: push ebp mov ebp, esp sub esp, 0x20 push ebx push esi push edi L2: mov ecx, 0x1 mov edx, 0x2 mov ebx, 0x3 mov esi, 0x4 mov edi, 0x5 mov eax, 0x6 mov [esp + 16], ecx mov ecx, 0x7 mov [esp + 20], edx mov edx, 0x8 mov [esp + 24], ebx xor ebx, ebx mov [esp + 28], esi mov esi, [esp + 16] add ebx, esi mov esi, [esp + 20] add ebx, esi mov esi, [esp + 24] add ebx, esi mov esi, [esp + 28] add ebx, esi add ebx, edi add ebx, eax add ebx, ecx add ebx, edx mov esi, [ebp + 8] mov [esi], ebx xor ebx, ebx mov esi, [esp + 16] sub ebx, esi mov esi, [esp + 20] sub ebx, esi mov esi, [esp + 24] sub ebx, esi mov esi, [esp + 28] sub ebx, esi sub ebx, edi sub ebx, eax sub ebx, ecx sub ebx, edx mov esi, [ebp + 12] mov [esi], ebx L3: pop edi pop esi pop ebx mov esp, ebp pop ebp ret
Je vidět, že vygenerovaný kód na některých místech ukládá proměnné do paměti a zpět, je to z toho důvodu, že množství proměnných přesáhlo množství registrů (to byl účel). Výpočet, který jsme provedli je sice velmi hloupý, ale na demonstraci alokace proměnných snad postačí.
Pokud by si někdo chtěl trochu experimentovat, tak kromě volby Function::setNaked() existuje ještě Function::setAllocableEbp(), která umožní alokovat ebp/rbp registr, který se normálně používá pro prolog/epilog. Vygenerovaný kód by vypadal takto:
L1: push ebx push ebp push esi push edi L2: mov ecx, 0x1 mov edx, 0x2 mov ebx, 0x3 mov ebp, 0x4 mov esi, 0x5 mov edi, 0x6 mov eax, 0x7 mov [esp - 12], ecx mov ecx, 0x8 mov [esp - 8], edx xor edx, edx mov [esp - 4], ebx mov ebx, [esp - 12] add edx, ebx mov ebx, [esp - 8] add edx, ebx mov ebx, [esp - 4] add edx, ebx add edx, ebp add edx, esi add edx, edi add edx, eax add edx, ecx mov ebx, [esp + 20] mov [ebx], edx xor edx, edx mov ebx, [esp - 12] sub edx, ebx mov ebx, [esp - 8] sub edx, ebx mov ebx, [esp - 4] sub edx, ebx sub edx, ebp sub edx, esi sub edx, edi sub edx, eax sub edx, ecx mov ebx, [esp + 24] mov [ebx], edx L3: pop edi pop esi pop ebp pop ebx ret
Můžeme si všimnout, že vygenerovaný kód je kratší a používá o něco málo míň přesunů mezi pamětí a registry. Sám používám tyto volby, ale nechal jsem je defaultně vypnuté, protože při nesprávném použití můžou být nebezpečné.
Chtěl bych napsat ještě jeden zápisek, kde bych zakončil své psaní o knihovně AsmJit a taky dokončil možnosti třídy Compiler.
Nezmínil jsem
Asi chyba v abc: při náhledu se &entity; převedou na html a není cesty zpět