jak sprawdzić które z tej listy rejestrów procesora są dostępne z poziomu c++
MMX
SSE
SSE2
SSE3
SSSE3
SSE4.1
SSE4.2
SSE4a
i jak to wygląda w praktyce o ile szybsze jest lizenie zwykłej matmy tzn dodawanie, odejmowanie, mnożenie i dzielenie kiedy są wstawione wstawki assemblerowe z wspomaganiem tych rejestrów których wymieniłem?
assembler i rejestry szybkiego liczenia
- Rejestracja: dni
- Ostatnio: dni
- Postów: 678
- Rejestracja: dni
- Ostatnio: dni
- Lokalizacja: Poznań
- Postów: 538
Jako, że nikt nie odpisał spróbuję pomoc. Nie znam się na tym ale ChatGPT napisał coś takiego:
1. Użycie makr kompilatora (np. w Visual Studio, GCC, Clang)
Te makra są definiowane automatycznie podczas kompilacji, jeśli kompilator wykryje, że może korzystać z danej technologii.
#ifdef __MMX__
std::cout << "MMX dostępne\n";
#endif
#ifdef __SSE__
std::cout << "SSE dostępne\n";
#endif
#ifdef __SSE2__
std::cout << "SSE2 dostępne\n";
#endif
#ifdef __SSE3__
std::cout << "SSE3 dostępne\n";
#endif
#ifdef __SSSE3__
std::cout << "SSSE3 dostępne\n";
#endif
#ifdef __SSE4_1__
std::cout << "SSE4.1 dostępne\n";
#endif
#ifdef __SSE4_2__
std::cout << "SSE4.2 dostępne\n";
#endif
2. Użycie CPUID (instrukcja procesora)
Możesz samodzielnie sprawdzić rejestry cpuid i wyciągnąć informacje o dostępnych rozszerzeniach.
#include <iostream>
#include <array>
#ifdef _MSC_VER
#include <intrin.h>
#else
#include <cpuid.h>
#endif
bool has_sse() {
std::array<int, 4> regs;
#ifdef _MSC_VER
__cpuid(regs.data(), 1);
#else
__cpuid(1, regs[0], regs[1], regs[2], regs[3]);
#endif
return (regs[3] & (1 << 25)) != 0; // SSE (bit 25 EDX)
}
bool has_sse2() {
std::array<int, 4> regs;
#ifdef _MSC_VER
__cpuid(regs.data(), 1);
#else
__cpuid(1, regs[0], regs[1], regs[2], regs[3]);
#endif
return (regs[3] & (1 << 26)) != 0; // SSE2 (bit 26 EDX)
}
Tabela bitów CPUID (eax = 1):
Bit | Rejestr | Znaczenie
23 | EDX | MMX
25 | EDX | SSE
26 | EDX | SSE2
0 | ECX | SSE3
9 | ECX | SSSE3
19 | ECX | SSE4.1
20 | ECX |SSE4.2
Zwykłe operacje CPU (bez SIMD):
for (int i = 0; i < N; ++i)
result[i] = a[i] + b[i];
SIMD (SSE):
for (int i = 0; i < N; i += 4) {
__m128 va = _mm_load_ps(&a[i]);
__m128 vb = _mm_load_ps(&b[i]);
__m128 vr = _mm_add_ps(va, vb);
_mm_store_ps(&result[i], vr);
}
- Rejestracja: dni
- Ostatnio: dni
- Postów: 1020
jak sprawdzić które z tej listy rejestrów procesora są dostępne z poziomu c++
MMX
SSE
SSE2
Zestaw instrukcji != rejestry. Najczęściej instrukcje SIMD muszą operować na określonym zestawie rejestrów, ale generalnie nie mówi się, że są dostępne jakieś rejestry SIMD tylko określony zestaw instrukcji SIMD jest dostępny dla danego procesora. Skoro jest dostępny to nie ma sensu mówienia o rejestrach, bo jak masz dostępne instrukcje to tak na logikę musi to jakoś działać bez wchodzenia w szczegóły
Czepiam się, bo z nowymi generacjami przychodzą nowe instrukcje SIMD a rejestry pozostają takie same. Albo dochodzi jakaś instrukcja SIMD np. POPCNT (policz zapalone bity w rejestrze), które w ogóle nie potrzebują żadnych specjalnych rejestrów SIMD
i jak to wygląda w praktyce o ile szybsze jest lizenie zwykłej matmy
Bardzo szybko, teoretyczny limit to oczywiście długość wektora. Oczywiście użycie instrukcji SIMD nie jest takie proste. Jako programista musisz zadbać o odpowiedni layout danych, żeby dało się zaaplikować SIMD. Czasami zaaplikowanie konkretnej instrukcji wiąże się z byciem sprytnym i używaniem instrukcji w trochę nieszablonowy sposób
kiedy są wstawione wstawki assemblerowe z wspomaganiem tych rejestrów których wymieniłem?
Wstawki assemblerowe są raczej upierdliwe i ich nikt nie używa. Lepiej używać zwykłych funkcji, które są intrinsicami (czyli takie wstawki bez schodzenia do assemblera). @tBane wrzucił przykład
Kompilatory mają też autowektoryzację, która czasami działa. Do osiągnięcia najlepszych wyników trzeba to niestety pisać samemu
- Rejestracja: dni
- Ostatnio: dni
- Postów: 678
ale powiedz mi tBane jak to wygląda teraz w praktyce.... czy szybciej liczy się matmę tymi rejestrami np o ile procent wydajniej jest? i dzięki za makra
- Rejestracja: dni
- Ostatnio: dni
- Lokalizacja: Poznań
- Postów: 538
Nie mam pojęcia. Zmierz czas wykonania kilku tysięcy operacji i porównaj sobie
- Rejestracja: dni
- Ostatnio: dni
- Postów: 678
ok dzięki a wiesz jak wywołać taką wstawkę z np SSE np dodawanie dwóch zeminnych?
- Rejestracja: dni
- Ostatnio: dni
czy szybciej liczy się matmę tymi rejestrami
Jak podmienisz pojedyńcze operacje, gdzie zamiast A * B zawołasz sobie intrinsic SIMD to najpewniej bez różnicy albo gorzej. Uzysk możesz mieć wtedy gdy masz nieco trudniejszą, ale wciąż względnie spójną matmę do wykonania, np. dot product, i zgrupujesz jakiś nieco większy zestaw wejściowy, np. potrzebujesz policzyć cztery razy dot product dla różnych danych. Wtedy możesz zaoczędzić cykle.
Także bez zmiany w reszcie kodu, poza samą matmą, to wyjdzie na to samo, cykl to cykl. A, że ludzie tworzący kompilatory to całkiem kumaci ludzie to nie wykluczone, że wyjdziesz na tym gorzej. To co SIMD umożliwia to przeoranie więcej danych jednym cyklem. Także błędne jest rozumowanie, że to są jakieś szybszy rejestry, czy szybsze operacje.
- Rejestracja: dni
- Ostatnio: dni
- Postów: 678
teraz rozumiem w czym rzecz, dzięki za info .. bez odbioru
- Rejestracja: dni
- Ostatnio: dni
- Postów: 2196
zainteresowanie ASM świadczy u początkującego programisty błędnym przekonaniem że sam zrobię to lepiej niz kompilator
trochę to podobne do koncepcji zbudowania systemu operacyjnego w ASM :D
w rzeczywistości nie da się bo ci co tworzą kompilatory znają sie na tym co robią
można spróbować zoptymalizować jakiś fragment nieźle się przy tym bawiąc ale efekty z tego będą mizerne
bardziej spektakularne efekty daje:
- optymalizacja kodu w c++ ( obejrzenie z punktu widzenia profilera aplikacji )
- zrównoleglenie procesu na wiele procesorów (np. OpenMP) albo korzystanie z wątków ze zrozumieniem
- uzycie cuda openCL
- Rejestracja: dni
- Ostatnio: dni
- Postów: 2541
Jak coś to matme liczy się głównie na GPU, bo tą czasochłonną matmą są operacje macierzowe. Większość przypadków komercyjnych dziś to modele AI i grafika komputerowa. Dodatkowo są akceleratory specjalnie stworzone do takich obliczeń.
No i to co @Marius.Maximus napisał takie rzeczy są ogarniane już przez kompilator w trakcie optymalizacji + zewnętrzne libki jak OpenCV, TensoRT, ONNX itd. Choć od siebie dodam, że w poprzedniej firmie natrafiłem na ręczne użycie wektoryzacji.
Tak więc warto przyswoić wiedzę o SIMD jak najbardziej, ale pisząc kod to najpierw zrób porządny algorytm a potem napisz go cache friendly, jeszcze później wielowątkowość, -O3 i dopiero na końcu takie cudowanie.
- Rejestracja: dni
- Ostatnio: dni
Czitels napisał(a):
Jak coś to matme liczy się głównie na GPU, bo tą czasochłonną matmą są operacje macierzowe. Większość przypadków komercyjnych dziś to modele AI i grafika komputerowa. Dodatkowo są akceleratory specjalnie stworzone do takich obliczeń.
Niekoniecznie, przykładowo niedawnymi usprawnieniami w .NET jest to że przepisali operacje na listach na operacje wektorowe na procesorze. Przy jednym obiegu pętli obrabiane jest kilka kolejnych wartości zamiast pojedynczo, ale jest to optymalizacja na poziomie frameworka po to żeby przeciętny programista nie musiał się tym zajmować. Nie wszystko da się / opłaca się liczyć na gpu. Fizyka w grach nadal jest przykładowo liczona na CPU, istnieje np do fizyki osobny układ nvidii PhysX ale jego rola znacznie spadła z czasem gdy procesory były wystarczające żeby to liczyć samemu, poza tym i tak trzeba było mieć fallback dla kart AMD więc podwajało to pracę developerów.
Dodatkowo wiele programów musi działać na serwerach gdzie w ogóle nie ma gpu, tak więc nadal jest sporo zastosowań dla tych zaawansowanych funkcji procesora, ale to głównie twórcy kompilatorów i niskopoziomowych frameworków powinni się tym przejmować.
- Rejestracja: dni
- Ostatnio: dni
- Postów: 70
Szybkie liczenie, ale to jest trochę błędne rozumowanie, bo odnosisz się do pojedynczej instrukcji, ale na około możesz pełno błędów zrobić jak cache spatial i temporal locality, gdzie odgrywa duże znaczenie dobre rozmieszczenie danych, żeby cache missów nie było i nie tylko danych, ale też kodu, bo program ma cache danych i cache instrukcji, żeby program nie skakał branchami w losowe miejsca, tylko najlepiej branchless wtedy wszystkie dane są obok siebie i żeby procesor nie musiał rollbackować źle spekulowanych instrukcji, a także żeby mógł lepiej przewidywać prawdopodobny branch jeśli już są, przy losowych danych procesor ma 50% szans wtedy może też źle spekulować, ale można posortować, czy nawet zaznaczyć unlikely kod jeśli możemy to przewidzieć, że jest mało prawdopodobny branch, który się wykona, lepiej żeby raz na tysiąc się pomylił, niż co chwilę.
Każda pomyłka procesora sprawia, że zamiast mieć te 4-5Ghz, spada ci taktowanie dużo dużo bardziej w dół, bo niby five stage pipeline pozwala wykonywać kilka microinstrukcji na raz przez co możesz każdą instrukcję praktycznie w 1 takcie zrobić to jeśli są różne problemy, rollbacki to spada ci i nie osiągniesz już tak wysokiego taktowania, a jakieś niższe np. 2Ghz.
Też procesor pozwala wykonać dwie instrukcje jednocześnie jeśli nie są od siebie zależne, to można to też wykorzystać na procesorach amd64.
No i wykorzystanie najlepiej wektorowych instrukcji.
Odwrócenie dzielenia na mnożenie, jakieś inne tricki, które wykonują logicznie podobną instrukcję liczenia, ale mogą spełniać nasze kryteria, ogólnie zawsze jak idzie to najlepiej użyć wzoru matematycznego no i optymalizować kod, więcej matmy, a mniej brute force, pętli z logiką.
W ogóle możesz algorytm zapisać grafowo i go skrócić, uprościć w niektórych przypadkach kilka instrukcji do jednej, albo redundantne usunąć.
Można skorzystać z GPU do obliczeń, ale trzeba sobie zdawać sprawę, że GPU jest szybsze dopiero jak jest naprawdę dużo tych danych bo sam transfer do, obliczenia i z odpowiedzią jest w dużej ilości przypadków wolniejsze od CPU, jedynie jak jest bardzo dużo danych i skomplikowane obliczenia to wtedy ten czas stracony na transfer się zwraca.
Więc nie zawsze się opłaca na GPU, zwłaszcza przy drobnych obliczeniach.
- Rejestracja: dni
- Ostatnio: dni
- Postów: 678
mam wersje na zwykłych rejestrach np dodawanie:
int a = 10, b = 5, result;
__asm {
mov eax, a // Przenieś wartość zmiennej a do rejestru EAX
mov ebx, b // Przenieś wartość zmiennej b do rejestru EBX
add eax, ebx // Dodaj zawartość EAX i EBX, wynik w EAX
mov result, eax // Przenieś wynik z EAX do zmiennej result
}
i teraz jak zrobić to samo w bloku klamrowym od __asm{} dodawanie na SIMD , chciałbym dodawanie , odejmowanie , mnożenie i dzielenie na SIMD w bloku __asm{}
napisałem takie coś:
int b = 1;
bool done=false;
FPS fps;
do
{
for(int i=0;i<1000000;)
{
__asm {
mov eax, i // Przenieś wartość zmiennej a do rejestru EAX
mov ebx, b // Przenieś wartość zmiennej b do rejestru EBX
add eax, ebx // Dodaj zawartość EAX i EBX, wynik w EAX
mov i, eax // Przenieś wynik z EAX do zmiennej result
}
}
cout << fps.getFps() << endl;
}while(!done);
teraz mozna porównywać wstawki która szybsza a która wolniejsza jak działa SIMD w c++ w bloku __asm{} ?
- Rejestracja: dni
- Ostatnio: dni
- Postów: 678
czy to jest to:
__asm {
movaps xmm0, i
movaps xmm1, b
addps xmm0, xmm1
movaps i, xmm0
}
dodawanie?
mój msvc++ 6.0 jest za stary , brak obsługi SIMD:
C:\projekty\konsola\konsola.cpp(636) : error C2400: inline assembler syntax error in 'opcode'; found 'xmm0'
C:\projekty\konsola\konsola.cpp(637) : error C2400: inline assembler syntax error in 'opcode'; found 'xmm1'
C:\projekty\konsola\konsola.cpp(638) : error C2400: inline assembler syntax error in 'opcode'; found 'xmm0'
C:\projekty\konsola\konsola.cpp(639) : error C2400: inline assembler syntax error in 'opcode'; found 'i'
Error executing cl.exe.
- Rejestracja: dni
- Ostatnio: dni
wilkwielki napisał(a):
jak sprawdzić które z tej listy rejestrów procesora są dostępne z poziomu c++
MMX
SSE
SSE2
SSE3
SSSE3
SSE4.1
SSE4.2
SSE4a
Jeśli nie targetujesz Windowsów starszych niż Windows 8 to możesz spokojnie założyć istnienie MMX, SSE i SSE2.
Nowsze instrukcje wypadałoby sprawdzić za pomocą CPUID.
Masz tu kod wygenerowany przez ChatGPT, wydaje się działać:
#include <iostream>
#include <intrin.h>
bool isSSE3Available() {
int cpuInfo[4];
__cpuid(cpuInfo, 1);
return (cpuInfo[2] & (1 << 0)) != 0;
}
bool isSSSE3Available() {
int cpuInfo[4];
__cpuid(cpuInfo, 1);
return (cpuInfo[2] & (1 << 9)) != 0;
}
bool isSSE41Available() {
int cpuInfo[4];
__cpuid(cpuInfo, 1);
return (cpuInfo[2] & (1 << 19)) != 0;
}
bool isSSE42Available() {
int cpuInfo[4];
__cpuid(cpuInfo, 1);
return (cpuInfo[2] & (1 << 20)) != 0;
}
// SSE4a is specific to AMD and is indicated by bit 6 of ECX in EAX=0x80000001
bool isSSE4aAvailable() {
int cpuInfo[4];
__cpuid(cpuInfo, 0x80000000);
if (cpuInfo[0] >= 0x80000001) {
__cpuid(cpuInfo, 0x80000001);
return (cpuInfo[2] & (1 << 6)) != 0;
}
return false;
}
// AVX is bit 28 of ECX in EAX=1 and requires OS support (via XGETBV)
bool isAVXAvailable() {
int cpuInfo[4];
__cpuid(cpuInfo, 1);
bool osUsesXSAVE_XRSTORE = (cpuInfo[2] & (1 << 27)) != 0;
bool avxSupport = (cpuInfo[2] & (1 << 28)) != 0;
if (osUsesXSAVE_XRSTORE && avxSupport) {
unsigned long long xcrFeatureMask = _xgetbv(_XCR_XFEATURE_ENABLED_MASK);
return (xcrFeatureMask & 0x6) == 0x6; // XMM (bit 1) and YMM (bit 2) state enabled
}
return false;
}
// AVX2 is bit 5 of EBX in EAX=7, ECX=0
bool isAVX2Available() {
int cpuInfo[4];
__cpuid(cpuInfo, 0);
if (cpuInfo[0] >= 7) {
__cpuidex(cpuInfo, 7, 0);
return (cpuInfo[1] & (1 << 5)) != 0;
}
return false;
}
int main() {
std::cout << "SSE3: " << (isSSE3Available() ? "Yes" : "No") << '\n';
std::cout << "SSSE3: " << (isSSSE3Available() ? "Yes" : "No") << '\n';
std::cout << "SSE4.1: " << (isSSE41Available() ? "Yes" : "No") << '\n';
std::cout << "SSE4.2: " << (isSSE42Available() ? "Yes" : "No") << '\n';
std::cout << "SSE4a: " << (isSSE4aAvailable() ? "Yes" : "No") << '\n';
std::cout << "AVX: " << (isAVXAvailable() ? "Yes" : "No") << '\n';
std::cout << "AVX2: " << (isAVX2Available() ? "Yes" : "No") << '\n';
return 0;
}
Uruchomiony na Celeronie J3455 daje output:
SSE3: Yes
SSSE3: Yes
SSE4.1: Yes
SSE4.2: Yes
SSE4a: No
AVX: No
AVX2: No
co wydaje się sensowne i zgodne z tym co wyświetla program CPU-Z.
- Rejestracja: dni
- Ostatnio: dni
- Postów: 678
zrobiłem nowy projekt i po wklejeniu kodu jest błąd
C:\programy\Microsoft Visual Studio\VC98\INCLUDE\ios(9) : fatal error C1083: Cannot open include file: 'streambuf': No such file or directory
Error executing cl.exe.
jak jest pusty szablon kodu i jest tylko sama #include <intrin.h> to jest ten sam błąd
C:\programy\Microsoft Visual Studio\VC98\INCLUDE\ios(9) : fatal error C1083: Cannot open include file: 'streambuf': No such file or directory
- Rejestracja: dni
- Ostatnio: dni
- Postów: 678
czemu to nie działa? operacje są związane z rejestrami MMX:
#include <windows.h>
#include <iostream.h>
#include <time.h>
#include <conio.h>
class FPS
{
public:
FPS();
~FPS();
LARGE_INTEGER start,end,pos,m_ticksPerSecond;
int getFps()
{
QueryPerformanceCounter(&start);
pos.QuadPart=start.QuadPart-end.QuadPart;
end.QuadPart=start.QuadPart;
int fps=(float)m_ticksPerSecond.QuadPart/(float)(pos.QuadPart);
return fps;
}
};
FPS::FPS()
{
QueryPerformanceFrequency(&m_ticksPerSecond);
}
FPS::~FPS()
{
}
void main()
{
int b = 1;
FPS fps;
while(true)
{
for(int i=0;i<256;)
{
/*__asm { // tutaj 72 tysiące fps
mov eax, i // Przenieś wartość zmiennej a do rejestru EAX
mov ebx, b // Przenieś wartość zmiennej b do rejestru EBX
add eax, ebx // Dodaj zawartość EAX i EBX, wynik w EAX
mov i, eax // Przenieś wynik z EAX do zmiennej result
}*/
__asm
{
movq mm0, i
movq mm1, b
paddd mm0, mm1
movq i,mm0
}
}
cout << "fps:" << fps.getFps() << endl;
}
}
efekt jest taki że jest i pisze zero fps, dziwna sprawa ...
- Rejestracja: dni
- Ostatnio: dni
- Lokalizacja: XML Hills
Marius.Maximus napisał(a):
zainteresowanie ASM świadczy u początkującego programisty błędnym przekonaniem że sam zrobię to lepiej niz kompilator
trochę to podobne do koncepcji zbudowania systemu operacyjnego w ASM :D
w rzeczywistości nie da się bo ci co tworzą kompilatory znają sie na tym co robią
można spróbować zoptymalizować jakiś fragment nieźle się przy tym bawiąc ale efekty z tego będą mizerne
w tym temacie jest mowa o użyciu wektorów simd, a w praktyce autowektoryzacja (czy automatyczne przerobienie kodu skalarnego na wektorowy przez kompilator) ssie i to mocno jeśli chodzi o nietrywialne pętle.
dla przykładu kodeki wideo (i ogólnie obróbka wideo) mocno korzysta na ręcznej wektoryzacji. dla przykładu:
- możecie sobie poszukać ile jest ręcznie optymalizowanego kodu asemblerowego bądź bezpośrednio użycia compiler intrinsics w https://gitlab.com/AOMediaCodec/SVT-AV1 (np. szukając avx2 w pull requestach: https://gitlab.com/AOMediaCodec/SVT-AV1/-/merge_requests/?sort=created_date&state=all&search=avx2&first_page_size=20 )
- analogicznie jest ffmpeg, gdzie ręczne optymalizacje też przyspieszają o rzędy wielkości: https://www.phoronix.com/news/FFmpeg-July-2025-AVX-512
oczywiście trzeba się znać na optymalizacji niskopoziomowej, żeby przegonić kompilator, np. tu jest sprawozdanie z procesu optymalizacji:
@wilkwielki :
skoro już używasz c++ to polecam na początek użyć jakiejś porządnej nakładki (abstrakcji) na ten cały simd, np: https://github.com/google/highway . zaletą google highway jest to, że jest przenośny, tzn. z jednego ręcznie zwektoryzowanego kodu c++ możesz wygenerować kod natywny zawierający instrukcje wektorowe dla wielu platform: x86, arm, risc-v, powerpc, webassembly, itp itd. jak już załapiesz jak się przyspiesza programy wektorami to możesz zejść niżej i używać tzw. compiler intrinsics. używanie instrukcji asemblerowych bezpośrednio ma sens dopiero jak dobrze ogarniasz szereg niskopoziomowych optymalizacji naraz, czyli nie tylko używanie instrukcji wektorowych, ale np. też zarządzanie rejestrami (np. tak żeby minimalizować koszt przenoszenia danych z i do rejestrów), zamiana kosztowych instrukcji na tańsze, redukcja obliczeń przez spamiętywanie czy uwspólnianie podwyrażeń, itp itd
hierarchia jest więc taka:
- przy trywialnych pętlach można polegać na autowektoryzacji
- jeżeli widzisz, że autowektoryzacja zawodzi (a często zawodzi) to użyj https://github.com/google/highway - żeby właściwie (z korzyścią wydajnościową) używać wektorów musisz co najmniej zrozumieć różnicę między drogimi operacjami redukcji (np. sumowanie elementów wektora jest drogie), a tanimi operacjami na kolejnych parach liczb z pary wejściowych wektorów (simdy są ogólnie tak skonstruowane, że te tanie instrukcje operują na takich osobnych parach liczb)
- jeżeli widzisz, że mimo właściwego użycia https://github.com/google/highway kompilator generuje znacznie gorszy kod niż taki, który mógłbyś sam osiągnąć to użyj wprost compiler intrinsics dla danego zbioru instrukcji (np. sse2, avx2, neon, sve3, itp itd) - to jest trudniejsze, bo semantyka intrinsics może być miejscami szalona i trzeba się mocno zastanowić nad każdym intrinsics przed próbą jego użycia
- jeżeli widzisz, że mimo właściwego użycia compiler intrinsics kompilator generuje znacznie gorszy kod niż taki, który mógłbyś sam osiągnąć, to zaklep kod wprost w asemblerze - to ogólnie wymaga dużego doświadczenia i znajomości charakterystyki mikroarchitekturalnej procesorów, bo masa rzeczy wpływa na szybkość wykonywania kodu