Бінарні оптимізації для щоденного використання

💡 Усі статті, обговорення, новини про Java — в одному місці. Приєднуйтесь до Java спільноти!

Привіт усім! Мене звати Максим Дудка, я Java Engineer. Сьогодні я покажу декілька простих бінарних оптимізацій, що можуть підвищити перформанс коду. На мій погляд, їхнє застосування має сенс завжди, коли це можливо з точки зору бізнес-логіки.

Статтю також можна прочитати англійською.

Побітові операції є фундаментальними інструментами у програмуванні через їхню ефективність. Основні побітові оператори:

  • AND ( & ): повертає true тільки тоді, коли обидва біти дорівнюють 1.
  • OR ( | ): повертає true , якщо принаймні один з бітів дорівнює 1.
  • XOR ( ^ ): повертає true тільки тоді, коли 1 біт дорівнює 1, а інший — 0.
  • NOT ( ~ ): змінює кожен біт (0 стає 1, а 1 стає 0).
  • Лівий Зсув ( << ): зсуває біти вліво, заповнюючи праворуч нулями.
  • Правий Зсув ( >> ): зсуває біти праворуч, зберігаючи знаковий біт для знакових чисел.

Ці операції забезпечують ефективну обробку даних на найнижчому рівні, часто призводячи до оптимізації продуктивності в різних застосуваннях, від простої маніпуляції даними до складних алгоритмічних рішень.

Приклади побітових операцій

Швидке множення та ділення

Клас QuickMaths демонструє використання побітових операторів лівого та правого зсуву для швидкого множення та ділення.

public class QuickMaths {
    public static int multiply(int num, int power) {
        return num << power;
    }
    public static int divide(int num, int power) {
        return num >> power;
    }
}

Пояснення роботи:

  • Оператор лівого зсуву << зсуває біти числа вліво на певну кількість позицій, визначену power. Кожен зсув вліво фактично множить число на два. Наприклад, зсув на один (num << 1) подвоює число, зсув на два (num << 2) множить його на чотири й так далі. Ця операція набагато швидша, ніж звичайна операція множення, особливо для великих чисел.
  • Навпаки, оператор правого зсуву >> зсуває біти числа вправо. Ця операція еквівалентна діленню числа на два для кожного зсуву. Зсув на один (num >> 1) ділить число навпіл, зсув на два (num >> 2) ділить його на чотири й так далі. Цей метод ділення, як і множення з лівим зсувом, значно швидший за традиційні методи ділення.

Перевірка числа на парність

Клас ParityCheck використовує побітову операцію AND для визначення парності числа.

public class ParityCheck {
    public static void checkParity(int num) {
        if ((num & 1) == 0) {
            System.out.println("Число парне.");
        } else {
            System.out.println("Число непарне.");
        }
    }
}

Пояснення роботи:

  • Побітова операція AND порівнює кожен біт двох чисел і повертає 1 для кожної позиції біта, де обидва біти дорівнюють 1. У контексті перевірки парності числа операція num & 1 є ключовою. Ця операція порівнює найменший значущий біт числа num з 1.
  • У двійковій системі всі парні числа закінчуються на 0 (наприклад, 2 — це 10, 4 — це 100), а всі непарні числа закінчуються на 1 (наприклад, 1 — це 01, 3 — це 11). Коли парне число виконує операцію AND з 1 (парне & 1), результат завжди 0, тому що найменший значущий біт парного числа — 0. Навпаки, коли непарне число виконує операцію AND з 1 (непарне & 1), результат завжди 1, тому що найменший значущий біт непарного числа — 1.
  • Цей метод перевірки парності високоефективний, оскільки він містить лише одну побітову операцію, яка безпосередньо перевіряє найменший значущий біт числа. Він уникає необхідності використання операцій ділення або залишку (наприклад, num % 2), які є більш обчислювально складними.

Перевірка, чи є число степенем двійки

Клас PowerOfTwoCheck використовує побітову операцію AND для визначення, чи є число степенем двійки.

public class PowerOfTwoCheck {
    public static void isPowerOfTwo(int n) {
        if (n > 0 && (n & (n - 1)) == 0) {
            System.out.println(n + " є степенем 2.");
        } else {
            System.out.println(n + " не є степенем 2.");
        }
    }
}

Пояснення роботи:

  • У двійковій формі число, що є степенем двійки, має точно один біт, встановлений у 1, а всі інші біти — у 0. Наприклад, 4 (100 у двійковому вигляді), 16 (10000 у двійковому вигляді) тощо. Ця унікальна властивість є ключем до їхньої ідентифікації.
  • Вираз n & (n — 1) є центральним у цьому методі. Коли n є степенем двійки, віднімання 1 перевертає всі біти після єдиного встановленого біта (зокрема цей біт). Наприклад, 4 у двійковій системі — це 100, а 3 (що є 4-1) — це 011. Операція AND 100 & 011 дає результат 000.
  • Якщо n не є степенем двійки, це означає, що в його двійковому представленні є додаткові встановлені біти, тому n & (n — 1) не буде нулем. Це тому, що віднімання 1 буде перевертати біти тільки до найменшого значущого встановленого біта, залишаючи інші встановлені біти незмінними.

Свап значеннь змінних без використання тимчасової змінної

Клас SwapVariables демонструє, як побітову операцію XOR можна використовувати для обміну значеннями двох змінних без потреби у тимчасовій змінній.

public class SwapVariables {
    public static void swap(int a, int b) {
        // Початкові значення: a = 5 (0101 у двійковому коді), b = 3 (0011 у двійковому коді)
        a = a ^ b; // a = 0101 ^ 0011 (0110 або 6 у десятковій системі)
        b = a ^ b; // b = 0110 ^ 0011 (0101 або 5 у десятковій системі, первісне значення a)
        a = a ^ b; // a = 0110 ^ 0101 (0011 або 3 у десятковій системі, первісне значення b)
        System.out.println("Після обміну: a = " + a + ", b = " + b);
    }
}

Пояснення роботи:

  • XOR є побітовою операцією, яка повертає 1, тільки якщо порівнювані біти різні. Коли одне і те саме значення піддається операції XOR з самим собою, результат завжди 0 (наприклад, x ^ x = 0). Крім того, XOR є комутативною та асоціативною, що означає, що порядок операцій не змінює результат (наприклад, a ^ b ^ a те саме, що b ^ a ^ a).
  • У першому кроці, a = a ^ b, a присвоюється результат XOR a та b. На цьому етапі a містить комбіновану інформацію обох a та b.
  • У другому кроці, b = a ^ b, тепер b присвоюється результат XOR нового a та оригінального b. Оскільки a є a ^ b, цей крок фактично повертає a до його первісного значення, присвоюючи його b.
  • У третьому кроці, a = a ^ b, a присвоюється результат XOR нового a (який є старим a ^ b) та нового b (який є оригінальним a). Цей крок фактично повертає b до його первісного значення, тепер присвоюючи його a.
  • Цей метод обміну значеннями змінних не тільки елегантний, але й ефективний, оскільки уникає необхідності використання тимчасової змінної.

Знаходження унікального елементу в масиві

Клас UniqueFinder демонструє, як можна використовувати побітову операцію XOR для знаходження унікального елементу в масиві, де кожен інший елемент зустрічається двічі.

public class UniqueFinder {
    public static int findUnique(int[] array) {
        int uniqueElement = 0;
        for (int element : array) {
            uniqueElement ^= element;
        }
        return uniqueElement;
    }
}

Пояснення роботи:

  • Особливістю XOR є те, що коли ви виконуєте XOR одного і того ж числа двічі, результат стає 0 (наприклад, x ^ x = 0). Ця властивість є важливою для цього методу.
  • Метод findUnique ініціалізує uniqueElement до 0, а потім виконує XOR з кожним елементом масиву. У масиві, де кожен елемент, крім одного, трапляється двічі, всі елементи, що трапляються двічі, в результаті XOR дають 0, залишаючи унікальний елемент як результат. Наприклад, у масиві [2, 3, 2] розрахунок буде 0 ^ 2 ^ 3 ^ 2, що спрощується до 3, оскільки 2 ^ 2 взаємно знищується.
  • Цей метод дуже ефективний, оскільки потребує лише одного проходу через масив, без потреби у вкладених циклах чи додаткових структурах даних.

Оптимізований масив булеанів (aka масив прапорців)

Клас BitwiseFlags використовує ціле число (flags) для представлення серії булевих прапорців. Кожен біт у цілому числі відповідає різному прапорцю, роблячи цей метод надзвичайно ефективним з точки зору простору, порівняно з використанням масиву boolean.

public class BitwiseFlags {
    private int flags;
    public void setFlag(int position) {
        flags |= (1 << position);
    }
    public boolean checkFlag(int position) {
        return (flags & (1 << position)) != 0;
    }
    public void clearFlag(int position) {
        flags &= ~(1 << position);
    }
}

Пояснення роботи:

  • Метод setFlag використовує побітову операцію OR (|) у поєднанні з лівим зсувом (<<). Вираз (1 << position) створює значення, де тільки біт на вказаній позиції встановлено в 1. Операція OR потім забезпечує, що цей біт встановлено у зміннній flags, залишаючи інші біти незмінними.
  • Для перевірки, чи встановлений певний прапорець, метод checkFlag використовує побітову операцію AND (&). Подібно до setFlag, вираз (1 << position) націлюється на конкретний біт. Якщо цей біт встановлений у flags, результат буде не нульовим (true); в іншому разі — нуль (false).
  • Метод clearFlag використовує побітову операцію AND у поєднанні з побітовою операцією NOT (~). Вираз ~(1 << position) створює значення, де всі біти встановлено в 1, крім біта на вказаній позиції. Операція AND потім очищає тільки цей конкретний біт у змінній flags.
  • Цей метод управління прапорцями не тільки ефективний з точки зору пам’яті, але й перформансу операцій встановлення, перевірки та очищення прапорців. Він уникає надмірного навантаження від достепу до окремих елементів масиву або об’єктів.

Висновок

Побітові операції з їхнім прямим маніпулюванням бітами пропонують альтернативний та ефективний підхід до вирішення деяких задач у програмуванні. Я навів приклади на Java, що демонструють практичність та ефективність побітових операцій у реальних сценаріях, роблячи їх незамінною частиною інструментарію програмного інженера.

👍ПодобаєтьсяСподобалось6
До обраногоВ обраному4
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Побітові операції є фундаментальними інструментами у програмуванні через їхню ефективність

Ефективність в чому?
Якщо треба супер перформанс і енергоефективність то джава не найкращий вибір.

А in-memory операції це настільки незначно по часу порівняно з мережею/базою даних, що воно не вартує погіршення читабельності коду.

Головне щоб код можна було легко читати, а оті побітові операції це як perl, працює але фіг поймеш що автор коду хотів зробити і як модифікувати.

Ефективність в чому?

В принципі бітові операції мають дві переваги: по-перше, вони працюють на кшталт SIMD операцій, коли одна команда може працювати одразу з декількома бітами. По-друге, це компактність зберігання, тому ми можемо для деяких масок побудувати lookup tables.

Якщо треба супер перформанс і енергоефективність то джава не найкращий вибір.

ООП не найкращій вибір, а Java... Код, який манипулює з бітами, буде +/- однаково транслюватися у машинні інструкції що на Сі, що на Java.

А in-memory операції це настільки незначно по часу порівняно з мережею/базою даних

У деяких задачах це так, але не в усіх. Не кажучи про те, що мережа та база даних не є обов’язковими компонентами.

Головне щоб код можна було легко читати, а оті побітові операції це як perl, працює але фіг поймеш що автор коду хотів зробити і як модифікувати.

Ну... досягається вправами...

SIMD — Single instruction multiple data тут ні дочого. Це так само може бути здвиг чи xor.

На кшталт SIMD. Що робить SIMD? Ну наприклад одна команда додавання виконує

int a[8];
int b[8];
int c[8];
for (int i=0; i<7; ++i) {
  c[i] = a[i] + b[i];
}

Добре, а що робить побітовий xor? Ну приблизно те саме:

bool a[8];
bool b[8];
bool c[8];
for (int i=0; i<7; ++i) {
   c[i] = a[i] ^^ b[i];
}

Тобто в якомусь сенсі використання бітових операцій нагадує SIMD.

Якщо до блоку коду йде

Пояснення роботи:

то ймовiрно така оптимiзацiя може й не варта

Дивлячись де, тут той не плюй в колодязь, може в якомусь хитровидуманому випадку, воно і знадобиться. Ніхто не знає як воно завтра повернеться, к тут знаєш от такий спосіб і отримуєш результат.

На захист автора. Бітовий масив — це просто прекрасне рішення, дивно, що, знаючи цю механіку, ми в своєму сервері використовуємо саме масив для груп прав користувачів. Дякую, що наштовхули на ідею. Проблема буде тільки в розмірності змінної, бо, наприклад, у нас прав майже 300 позицій і треба буде розбивати їх на умовні блоки по 64.

Але, навіть враховуючи це, швидкість виросте точно, бо це як раз оптимізація алгоритму, вона дасть можливість перевіряти декілька прав за одну операцію, замість N викликів функцій (де будуть критичні секції, туди-сюди робота зі стеком і діставання даних зі структур).

Дякую.

P.S. У нас не Java.

На захист автора. Бітовий масив — це просто прекрасне рішення, дивно, що, знаючи цю механіку, ми в своєму сервері використовуємо саме масив для груп прав користувачів. Дякую, що наштовхули на ідею. Проблема буде тільки в розмірності змінної, бо, наприклад, у нас прав майже 300 позицій і треба буде розбивати їх на умовні блоки по 64.

Але, навіть враховуючи це, швидкість виросте точно, бо це як раз оптимізація алгоритму, вона дасть можливість перевіряти декілька прав за одну операцію, замість N викликів функцій (де будуть критичні секції, туди-сюди робота зі стеком і діставання даних зі структур).

Дякую.

P.S. У нас не Java.

Не велосипедьте, структура данних яку ви шукаєте так і зветься BitSet.
Можна знайти реалізацію фактично для будь якої мови програмування.
В Java -docs.oracle.com/...​ase/java/util/BitSet.html
В С++ — en.cppreference.com/w/cpp/utility/bitset
В Python — pypi.org/project/bitsets
В TypeScript — www.npmjs.com/...​et?activeTab=dependencies

На жаль, у нас Delphi. І для нього я такого класу не знайшов, а те, що є в 11-й версії — обмежене. Тому прийдеться писати самому.

У Pascal 30-річної давнини був set of на рівні мови.

То швидше enum в С++ термінах.

var X, Y, Z: set of Char;
begin
    X := ['1', '2'];
    Y := ['Q', 'W', 'E'];
    Include(X, '0');
    Z := X + Y;
    Y = Y * ['W'];

Ну тут так працює, як std::set

Так, але std::set працює з довільним типом даних, а set of працює лише з перелічувальними, тому що там під капотом бітова маска.

Множини добра і швидка штука, якщо елементів до 255. Якщо більше — то не підходить.

В Lazarus ніби є TBitset. Мої співчуття з нікро-технології. Колись я був її адептом доки не поставив 2005 ту версію та констатував, що усьо. Вже диплом писав на дебілдері 6-му тобто С++. Класна фірма Borland була, винайшли IDE.

Дякую, подивлюся, що там у них під капотом.

Нагадаю, шо в java є bitset docs.oracle.com/...​ase/java/util/BitSet.html для бітових операцій

Ну... там навіть доповнення немає. По суті це встановити біт, скинути, прочитати. Таке... Знову ж таки, припустимо є код, який працює з бітами:

static inline uint64_t ncm(uint64_t x)                
{                                                                       
    uint64_t a = x & -x;                                                
    uint64_t b = x + a;                                                 
    uint64_t c = b ^ x;                                                 
    a <<= 2;                                                             
    c /= a;                                                            
    return b | c;                                                       
}

Як його переписати з використанням BitSet?

Та мабуть не треба його переписувати бо він не для цього, не для математики. Наприклад для one hot encoding чи може Bloom filter, бо він може бути будь якого розміру.

Поради про ефективність без заміру ефективності це не дуже добре, якщо взагалі не погано. Я маю багато досвіду роботи з перфоманс проблемами у eCommerce і ні разу не стикався навіть з натяком на те що подібні операції займали хоча б якусь 10 долю відсотка повільної операції. Зате можна точна сказати що більшості програмістів знадобиться суттєвий час щоб навчитися читати цей «оптимізований» код. Тому радити таке написання коду я точно, м’яко кажучи, не став би.

Будь ласка завжди аргументуйте свою точко зору добре проведеними експериментами з статистикою. Тестування перфомансу бажано завжди робити у вигляді A/B тесту, використовуючи усі стандарти A/B тестування, добре зараз є чат GPT який швидко може про все це розповісти.

Так само маю довгий досвід в ecomerce, і усяке бачів. З останнього премутейшени були.

Окрім x & (x-1) ще може бути корисним x & -x.

Ну а так мені подобається наступний приклад: отримати наступе число, у якому така ж сама кількість бітів встановлена в одиницю. Наприклад, для одного біту це буде послідовність 1 -> 2 -> 4 -> 8 -> 16, для двох біт це 3 -> 5 -> 6 -> 9 -> 10 -> 12 -> 17 ...

uint64_t next_combination(uint64_t x);

Це може бути корисним, щоб, наприклад, перебрати усі варіанти п’яти карт з 52-колоди. Перебрати усі бітові маски тоді можна за допомогою циклу

uint64_t mask = 0x1Full; // Five bits
uint64_t last = 1ull << 52; // 52-card deck overflow
while (mask < last) {
  /* Process mask */
  mask = next_combination(mask);
}

Придумаємо алгоритм, як це зробити. Припустимо ми маємо маску xxxxx01111000. Яка має бути наступною? xxxxx1000111, де x це довільні біти. Таким чином, нам треба знайти першу послідовність одиниць, саму ліву одиничку послідовності зсунути ліворуч, а усі інші одинички у послідовності зсунути праворуч на початок. Тепер треба це запрограмувати

Ок, Пригадуємо наш хак, який повертає самий правий встановлений біт uint64_t a = x & -x;

x = xxxxx01111000
a = 0000000001000

Тепер, додаємо uint64_t b = x+ a;

x = xxxxx01111000
a = 0000000001000
b = xxxxx10000000

Тобто це місце, куди зсується самий лівий біт. Добре. Тепер нам треба отримати послідовність одиничок, які будемо зсувати вправоруч до упора. Для цього можна просто узяти uint64_t c = x ^ b;

x = xxxxx01111000
a = 0000000001000
b = xxxxx10000000
c = 0000011111000

Тільки одиничок на дві більше, ніж треба. Плюс їх треба зсунути праворуч... Ну... білдіни, які можуть отримати індекс першого встановленого біту __builtin_ctz, потім можна зсунути... Але... Можна оскільки зсув це ділення, то можна замість зсуву його використати, тим більше, що у нас уже є a: uint64_t d = c / a;

x = xxxxx01111000
a = 0000000001000
b = xxxxx10000000
c = 0000011111000
d = 0000000011111

Так, дві одинички зайві, тому uint64_t e = d >> 2;

x = xxxxx01111000
a = 0000000001000
b = xxxxx10000000
c = 0000011111000
d = 0000000011111
e = 0000000000111

Ну і все, потрібний нам результат можна, якщо поєднати b та e: uint64_t f = b | e;

x = xxxxx01111000
a = 0000000001000
b = xxxxx10000000
c = 0000011111000
d = 0000000011111
e = 0000000000111
f = xxxxx10000111

Все, return f; без умов та циклів. А якщо використовувати __builtin_ctz, то там взагалі кожна інструкція декілька тактів.

Цей метод обміну значеннями змінних не тільки елегантний, але й ефективний, оскільки уникає необхідності використання тимчасової змінної

Обмін з проміжною змінною на регістрі на сучасних процесорах відбувається ефективніше. Взагалі, більшість перелічених трюків нині неактуальні, так як компілятори вміють їх формувати за потреби автоматично.

А мені сподобалось. Стаття академічного характеру, а не порада прямого застосування крізь і всюди.

А мені сподобалось. Стаття академічного характеру

Я просто неможу :)
Назвати вступну оглядову статтю академічною — це просто епіка

А Богдан проти, власне як завжди.

Ну.. розділпро біти у четвертому томі TAoCP так, трохи академічна... А це... Академічність це завжди претензія на повноту.

А Богдан проти, власне як завжди.

А Віктор проти, власне як завжди — чудовий аргумент і підхід: якщо не наводити аргументацію, то її не спростують.
Може розкажете, як ця стаття відповідає хоча б базовим вимогам до академічних статей?

uk.wikipedia.org/...​/Академічна_доброчесність Так ви самі не наводите жодних аргументів, що матеріал є академічно не доброчесним. Те що він рівня «простий» не робить його не достовірним чи плагіатом чи якимось іншим чином порушуючим етику. P.S. При усіх інших токсити та пенити в коментарях , при чому часто не по ділу, це у вас розвага така. Якось, один розумний модератор зі Stack Overflow на одні з моїх перших відповідей на питання, дав дуже цінний коментар — що пояснювати людям де їх помилки теж треба обережно. (Нажаль не типово для Українців взагалі, ми відверті грубіяни).

uk.wikipedia.org/...​/Академічна_доброчесність Так ви самі не наводите жодних аргументів, що матеріал є академічно не доброчесним.
Те що він рівня «простий» не робить його не достовірним чи плагіатом чи якимось іншим чином порушуючим етику.

Раніше не помічав, що ви — клоун :)
Академічна доброчесніть не є синонімом «академічної статті», від якої очікується певний рівень аргументації (ну хоча б якісь виміри) і якості (перший же коментар в темі dou.ua/...​511/?from=fortech#2747264).

Це стаття джуна для свого бложика, а не стаття академічного характеру. І смішно та сумно тут те, що знаходяться люди, які не здатні розділити ці поняття. Бітові операція — академічний контент, навіть більше магія, невідомі технології!

Не ганьбіться. Якщо ви маєте чітке визначення поняття «академічна стаття», критерії за якими це можна визначити — то надайте його і за пунктами покажіть, що стаття не відповідає визначенню «академічна». Бо ми з вами напевно не академіки, навідміну від киви з януковичем чи спартаком субботою.
Якщо ви можете надати спростування викладеного матеріалу, та надати рекомендації як то Роберта Мартін десь в Чистий Код або Фаулер в Рефакторінг — «Не випендрюйтесь в коді, бо він стає схожий на каракулі і його неможливо людині читати, та відповідно підтримувати » - то робіть те.

Я просто неможу :)
Назвати вступну оглядову статтю академічною — це просто епіка

Для сучасних джунів все що не REST/Js/NoSQL це вже академічний рівень)

Для сучасних джунів все що не REST/Js/NoSQL це вже академічний рівень)

Це при тому, що сам РЕСТ як архітектурний підхід — це якраз академічна штука (але дисер Філдінга навряд чи багато хто читав). Але на жаль у багатьох і не лише джунів максимум академічності при розмові про РЕСТ — це «ну там є __імпотентні__ методи»

Сьогодні я покажу декілька простих бінарних оптимізацій, що можуть підвищити перформанс коду.

Можуть. А можуть і не підвищити. Ви робили мікробенчмарки? Ви перевіряли вплив таких оптимізацій на систему в цілому?

Тести, які я робив десь 10 років тому, показали навіть гірші результати (вже на скомпільованому JIT коді), бо джава машина сама робить оптимізації, маючи більше контексту. А джава машину оптимізують під «загальні сценарії використання».
І попередній факт приводить нас до ще однієї проблеми — зрозумілість коду. Запропоновані оптимізації колись викладались на першому курсі, зараз вже менше, як результат зростає і час на читання коду неофітами, і ризик внесення помилок.

Я навів приклади на Java, що демонструють практичність та ефективність побітових операцій у реальних сценаріях, роблячи їх незамінною частиною інструментарію програмного інженера.

Знову ж дуже хочеться побачити результати вимірів. Бо дані оптимізації ок у всяких ембедедах та низькорівнемову програмуванні, де вони реально призведуть до «роботи коду на регістрах процесора», з високорівнемиви екосистемами прискорення дуже малоймовірне.

Ну... Можна брати, наприклад, генератори ходів у шахах. Наприклад тут Comparing 4 move generators: 0×88 vs 10×12 vs 10×12 + bitboards HYBRID vs Pure MAGIC BITBOARDS

Наприклад, у тесті 0x88 це 20842 ms, magic bitboards це 7140 ms. Тобто майже у три рази. Плюс треба дивитися, чи в разі bitboards використовується інстукції типа PEXT.

Наприклад, у тесті 0×88 це 20842 ms, magic bitboards це 7140 ms.
Бо дані оптимізації ок у всяких ембедедах та низькорівнемову програмуванні, де вони реально призведуть до «роботи коду на регістрах процесора», з високорівнемиви екосистемами прискорення дуже малоймовірне.

Але можете спробувати, але не забувайте, що треба пройти джит компіляцію

А як вона допоможе? Перетворить масив на бітові маски? Хоча б теоретично.

А як вона допоможе? Перетворить масив на бітові маски? Хоча б теоретично.

Теоретично і на практиці:
— часто там де можна провести оптимізації з заміною на бітові операції джит сам це робить
— робота з булівськими масивами теж часто оптимізується

Але ще є інший важливий момент: отакий код з бітовими оптимізаціями може не скомпілюватись, тому буде повільнішим ніж зоптимізований «простий код»

Зрозумілість коду залежить від досвіду та скілзів, тому це суб’єктивно.

Ніт. Зрозумілість коду — це класичний приклад нечіткого значення, тому визначається по мінімуму. Тобто, якщо джуну або середнячку не зрозумілий код, то ваш код є незрозумілим (як результат, ще й ціна його підтримки стає вищою, бо дешевих не наймеш).
Ну і www.laws-of-software.com/laws/kernighan

часто там де можна провести оптимізації з заміною на бітові операції джит сам це робить

Ну чесно, як замість

enum piece { WHITE_PAWN, BLACK_PAWN, WHITE_KNIGHT, BLACK_KNIGHT, WHITE_BISHIOP, ... }
struct position
{
    enum piece squares[64];
};

JIT компілятор згенерує

struct position
{
  uint64_t white_pawns;
  uint64_t black_pawns;
  uint64_t white_knights;
  uint64_t black_knigths;
  uint64_t white_bishops;
  ...
};

Біти це зовсім інший погляд на проблему.

Ніт. Зрозумілість коду — це класичний приклад нечіткого значення, тому визначається по мінімуму. Тобто, якщо джуну або середнячку не зрозумілий код, то ваш код є незрозумілим (як результат, ще й ціна його підтримки стає вищою, бо дешевих не наймеш).

Ну... тоді навіщо взагалі вартісні, коли можна найняти дешевих? А так це залежить від предметної галузі, і взагалі є купа нюансів. Інколи код простий, але підтримувати його важко. Інколи треба зозібратися з кодом, але після цього з ним легко працювати.

struct position
{
enum piece squares[64];
};

Це не валідний джава код, якщо що. Звідки ви взяли результат роботи джита? Є сумнів що він саме такий, враховуючи, що таке значення енуму в джаві.

Я не розумію, що ви хочете показати.

Це сішний код, бо я принципі не знаю, що таке Java. Ок, можете думати, що там щось на кштал

struct position
{
    uint8_t squares[64];
};

А хочу я сказати, що у більшості випадків JIT буде просто безсилий переробити алгоритм та презентацію даних з масивів на біти.

Це сішний код, бо я принципі не знаю, що таке Java.
Бо дані оптимізації ок у всяких ембедедах та низькорівнемову програмуванні, де вони реально призведуть до «роботи коду на регістрах процесора», з високорівнемиви екосистемами прискорення дуже малоймовірне.

---

А хочу я сказати, що у більшості випадків JIT

Тобто ви не знаєте як працює якась система (в вашому випадку джава), але робите якісь висновки і пробуєте щось доводити. Нятненько

Тобто ви не знаєте як працює якась система (в вашому випадку джава), але робите якісь висновки і пробуєте щось доводити. Нятненько

Тому що якщо припустити, що таке існує, ще й працює на кожному комп’ютері, то програмісти вже давно не були би потрібні взагалі, а ChtGPT визивав лише посмішку.

Я не знаю як працює саме JIT, але я знаю, де знаходиться межа оптимізації. Якщо ви впевнені, що з алгоритму 0×88 JIT може побудувати magic bitboards, то я можу сказати, що це божевілля.

Там трохи не так, по приблизно такому коду спочатку пройдеться високорівнений компілятор з : Java, Groovy, Scala, Kotlin в т.п. мови високого рівня. Цей код буде фронтендом компілятора розкладено в AST проведені базові оптимізації, та згенерований проміжний — байт код. Потім вже байт код буде інтерпретуватись віртуальною машиною і далі оптимізуватись по ходу роботи програми. При чому лише якщо певний проміжок коду буде використовуватись більше ніж певну кількість разів. Тим не меньше, ця система в цілому по низці замірів працює зі швидкістю як і безпосередньо скомпільований код з низки мов як то : Go lang чи Delphi. Разом з тим JVM використовує принаймні в 6 разів більше пам’яті, за нативний код. (З С/С++ звісно нема чого порівнювати, там оптимізації та в сам рівень коду не досяжні для інтепритаторів, але без бібліотеки типу QT чи POCO розробка дуже трудомістка) Та всеодно значно випереджає скажімо JavaScript чи TypeScript — в середньому подекуди в 20 разів. Чому тоді Node — комерційно витісняє Java з усіх ринків? Відповідь проста маркетинг у Google значно потужніший ніж в Oracle. Судова тяжба між Oracle та Google за спадок Sun, призвела до комерційного згасання платформи та стеку технологій.

І попередній факт приводить нас до ще однієї проблеми — зрозумілість коду.

Зрозумілість коду залежить від досвіду та скілзів, тому це суб’єктивно. Якщо то розумієш біти, то код досить зрозумілий. А цикли завжди треба парсити...

А ви можете навести реальні приклади з телеметрією, де чітко видно що JavaC чи JIT JVM зробили зазначені оптимізації коду? А також скільки додаткових команд процесора і відповідно тактів та часу пішло на саму цю мікро оптимізацію ? Щодо аргументів про ясність коду, тут з одного боку я погоджуюсь з іншого боку кажу, що це напевно зовсім про інше і описується швидше в роботах : Роберта Мартіна та інших авторів і по суті є окремою дисципліною — про структурну якість коду.

А ви можете навести реальні приклади з телеметрією, де чітко видно що JavaC чи JIT JVM зробили зазначені оптимізації коду?
Тести, які я робив десь 10 років тому,

А тепер подумайте на кому лежить тягар доведення: на тому хто висунув твердження чи на тому хто поставив його під сумнів?

Навіть більше:

зазначені оптимізації коду

Я не зазначав ніяких конкретних оптимізацій, а лише сказав, що вони існують. Ви не в курсі, що джит робить оптимізацію коду?

П.С. Ось тут ви звинувачуєте dou.ua/...​511/?from=fortech#2747744

При усіх інших токсити та пенити в коментарях , при чому часто не по ділу, це у вас розвага така.

Ви впевнені, що не сплутали мене і себе.
Власне, що ви хочете показати вашими коментарями, окрім невдалої спроби вигуляти біле пально, не ясно.

Цікаво і корисне знання, але це потрібно робити лише коли є інтенсивні обчислення великої кількості даних. Інакше це лише забруднює код, додає багів та і бенчмарком не впіймати покращення перформансу, це на межі похибки буде.

Читав у такого собі Крісла Касперскі він де Мищьх в «Техника оптимизации програм». Найбільший результат в оптимізації — дає оптимізація алгоритму, може призвести до порядкових покращень. Далі йде вже середне рівнева (розкрутка циклів, оптимізація переходів тощо) та з рештою низкорівневі оптимізації які діють як правило на часто використовуємих функціях, як то копіювання блоків пам’яті чи пошук символа в строці наприклад. Як безпосередньо оптимізувати програмний продукт дуже добре написано в Томаса Кайта (але на практиці мені показано колегою з Канади бо книги то добре, але ще треба практика). Коротше окрема дисципліна , про яку варто напевно написати обзорну статтю — де та яки матеріали дивитись.

public static int multiply(int num, int power) {
return num << power;
}

Повертає −16 для значень num = −2 та power = 3.

Я вже не кажу про те, що ці рекомендації — це фактично захаращування коду тими оптимізаціями, які java оптимізатор і так зробить. Тобто це навіть не premature optimisation, це obsolete optimisation.

А, ще забув додати, що створення об"єктів та виклики функцій це теж не безкоштовні операції, якщо шо.

Підписатись на коментарі