У Java, чи ефективніше використовувати байт чи короткий замість int та float замість double?


91

Я помітив, що завжди використовував int і double, незалежно від того, наскільки маленьким чи великим має бути число. Тож у Java, чи ефективніше використовувати, byteчи shortзамість, intа не floatзамість double?

Тож припустимо, що у мене є програма з великою кількістю інтів та дублів. Чи варто було б переглядати та міняти мій ints на bytes або shorts, якби я знав, що номер підходить?

Я знаю, що у Java немає типів без підпису, але чи є щось додаткове, що я міг би зробити, якби знав, що число буде лише позитивним?

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

(Я б припустив, що збирач сміття має справу лише з Об'єктами, а не примітивним, але все-таки видаляє всі примітиви в покинутих об'єктах, чи не так?)

Я спробував це з невеликим додатком для Android, який у мене є, але насправді не помітив різниці взагалі. (Хоча я нічого "науково" не вимірював).

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

Чи варто було б це робити з самого початку, коли я починаю новий проект? (Я маю на увазі, я думаю, що кожна дрібниця допомогла б, але знову ж таки, якщо так, чому, здається, це не робить ніхто)

Відповіді:


107

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

Коротка відповідь

Так, ви помиляєтесь У більшості випадків це мало впливає на використання простору.

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

Довша відповідь

Віртуальна машина Java моделює стеки та поля об’єктів, використовуючи зсуви, які є (по суті) кратними 32-бітному примітивному розміру комірки. Отже, коли ви оголошуєте локальну змінну або поле об'єкта як (скажімо) a byte, змінна / поле зберігатиметься в 32-бітовій комірці, як і int.

З цього є два винятки:

  • longа для doubleзначень потрібні 2 примітивні 32-розрядні комірки
  • масиви примітивних типів представлені в упакованому вигляді, так що (наприклад) масив байтів вміщує 4 байти на 32-бітове слово.

Тож, можливо, варто оптимізувати використання longта double... та великі масиви примітивів. Але загалом ні.

Теоретично JIT може бути в змозі оптимізувати це, але на практиці я ніколи не чув про JIT, який це робить. Одне перешкода полягає в тому, що JIT, як правило, не може працювати, поки не будуть створені екземпляри компільованого класу. Якби JIT оптимізував розташування пам'яті, ви могли б мати два (або більше) "смаки" об'єкта одного класу ... і це створило б величезні труднощі.


Перегляд

Дивлячись на результати тесту у відповіді @ meriton, виявляється, що використання shortта byteзамістьint вимагає покарання за продуктивність за множення. Дійсно, якщо розглядати операції ізольовано, то штраф є суттєвим. (Не слід розглядати їх окремо ... але це вже інша тема.)

Я думаю, що пояснення полягає в тому, що JIT, мабуть, виконує множення, використовуючи 32-бітні інструкції множення в кожному випадку. Але у випадку byteі short, він виконує додаткові інструкції для перетворення проміжного 32-бітового значення в ітерацію byteабо shortв кожну ітерацію циклу. (Теоретично це перетворення могло бути здійснено один раз в кінці циклу ... але я сумніваюся, що оптимізатор зміг би це зрозуміти.)

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


30
+1 не оптимізуйте, якщо у вас немає чітких доказів проблеми з продуктивністю
Bohemian

Е-м, чому JVM повинен чекати компіляції JIT, щоб упакувати макет пам'яті класу? Оскільки типи полів записуються у файл класу, чи не могло JVM вибрати макет пам'яті під час завантаження класу, а потім розпізнавати імена полів як байт, а не як зміщення слів?
meriton

@meriton - Я впевнений , що макети об'єктів будуть визначені під час завантаження класу, і вони не змінюються після цього. Дивіться частину моєї відповіді з «дрібним шрифтом». Якби фактичні макети пам'яті змінилися під час JIT-коду, JVM було б дуже важко мати справу. (Коли я сказав, що JIT може оптимізувати макет, це гіпотетично і непрактично ... що може пояснити, чому я ніколи не чув про те, щоб JIT насправді це робив.)
Стівен С

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

@meriton - У специфікації JVM йдеться про "зміщення слів віртуальної машини" в локальних кадрах / об'єктах. Яким чином вони відображаються на фізичні компенсації машини НЕ вказується. Дійсно, він не може це вказати ... оскільки можуть бути вимоги до вирівнювання полів, специфічних для апаратного забезпечення.
Stephen C

29

Це залежить від реалізації JVM, а також базового обладнання. Більшість сучасних апаратних засобів не отримують окремі байти з пам'яті (або навіть з кешу першого рівня), тобто використання менших примітивних типів, як правило, не зменшує споживання пропускної здатності пам'яті. Так само сучасні процесори мають розмір слова 64 біти. Вони можуть виконувати операції з меншими бітами, але це працює, відкидаючи зайві біти, що теж не швидше.

Єдина перевага полягає в тому, що менші примітивні типи можуть призвести до більш компактного розміщення пам'яті, особливо при використанні масивів. Це економить пам’ять, що може покращити локальність посилання (тим самим зменшивши кількість пропусків кешу) та зменшити накладні витрати на збір сміття.

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

Щоб продемонструвати це, ось такий орієнтир:

package tools.bench;

import java.math.BigDecimal;

public abstract class Benchmark {

    final String name;

    public Benchmark(String name) {
        this.name = name;
    }

    abstract int run(int iterations) throws Throwable;

    private BigDecimal time() {
        try {
            int nextI = 1;
            int i;
            long duration;
            do {
                i = nextI;
                long start = System.nanoTime();
                run(i);
                duration = System.nanoTime() - start;
                nextI = (i << 1) | 1; 
            } while (duration < 100000000 && nextI > 0);
            return new BigDecimal((duration) * 1000 / i).movePointLeft(3);
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }   

    @Override
    public String toString() {
        return name + "\t" + time() + " ns";
    }

    public static void main(String[] args) throws Exception {
        Benchmark[] benchmarks = {
            new Benchmark("int multiplication") {
                @Override int run(int iterations) throws Throwable {
                    int x = 1;
                    for (int i = 0; i < iterations; i++) {
                        x *= 3;
                    }
                    return x;
                }
            },
            new Benchmark("short multiplication") {                   
                @Override int run(int iterations) throws Throwable {
                    short x = 0;
                    for (int i = 0; i < iterations; i++) {
                        x *= 3;
                    }
                    return x;
                }
            },
            new Benchmark("byte multiplication") {                   
                @Override int run(int iterations) throws Throwable {
                    byte x = 0;
                    for (int i = 0; i < iterations; i++) {
                        x *= 3;
                    }
                    return x;
                }
            },
            new Benchmark("int[] traversal") {                   
                @Override int run(int iterations) throws Throwable {
                    int[] x = new int[iterations];
                    for (int i = 0; i < iterations; i++) {
                        x[i] = i;
                    }
                    return x[x[0]];
                }
            },
            new Benchmark("short[] traversal") {                   
                @Override int run(int iterations) throws Throwable {
                    short[] x = new short[iterations];
                    for (int i = 0; i < iterations; i++) {
                        x[i] = (short) i;
                    }
                    return x[x[0]];
                }
            },
            new Benchmark("byte[] traversal") {                   
                @Override int run(int iterations) throws Throwable {
                    byte[] x = new byte[iterations];
                    for (int i = 0; i < iterations; i++) {
                        x[i] = (byte) i;
                    }
                    return x[x[0]];
                }
            },
        };
        for (Benchmark bm : benchmarks) {
            System.out.println(bm);
        }
    }
}

який друкується на моєму дещо старому блокноті (додавання пробілів для налаштування стовпців):

int       multiplication    1.530 ns
short     multiplication    2.105 ns
byte      multiplication    2.483 ns
int[]     traversal         5.347 ns
short[]   traversal         4.760 ns
byte[]    traversal         2.064 ns

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


3
Замість того, щоб говорити "найвизначніше при використанні масивів", я думаю, що це може бути простіше сказати shortі byteє більш ефективним, якщо зберігається в масивах, які мають достатньо велике значення (чим більший масив, тим більша різниця в ефективності; byte[2]може бути більше або менш ефективний, ніж int[2], але недостатньо, щоб мати значення в будь-якому випадку), але що окремі значення ефективніше зберігаються як int.
supercat

2
Те, що я перевірив: Ці тести завжди використовували int ('3') як операнд фактору або призначення (варіант циклу, а потім кастований). Що я зробив, це використовував набрані фактори / операнди призначення в залежності від типу lvalue: int mult 76.481 ns int mult (typed) 72.581 ns short mult 87.908 ns short mult (typed) 90.772 ns byte mult 87.859 ns byte mult (typed) 89.524 ns int [] trav 88.905 ns int [] trav (typed) 89.126 ns short [] trav 10.563 ns short [] trav (typed) 10.039 ns byte [] trav 8.356 ns byte [] trav (typed) 8.338 ns Думаю, існує багато непотрібного кастингу. ці тести запускались на вкладці android.
Бондакс,

5

Використання byteзамість intможе збільшити продуктивність, якщо ви використовуєте їх у величезній кількості. Ось експеримент:

import java.lang.management.*;

public class SpeedTest {

/** Get CPU time in nanoseconds. */
public static long getCpuTime() {
    ThreadMXBean bean = ManagementFactory.getThreadMXBean();
    return bean.isCurrentThreadCpuTimeSupported() ? bean
            .getCurrentThreadCpuTime() : 0L;
}

public static void main(String[] args) {
    long durationTotal = 0;
    int numberOfTests=0;

    for (int j = 1; j < 51; j++) {
        long beforeTask = getCpuTime();
        // MEASURES THIS AREA------------------------------------------
        long x = 20000000;// 20 millions
        for (long i = 0; i < x; i++) {
                           TestClass s = new TestClass(); 

        }
        // MEASURES THIS AREA------------------------------------------
        long duration = getCpuTime() - beforeTask;
        System.out.println("TEST " + j + ": duration = " + duration + "ns = "
                + (int) duration / 1000000);
        durationTotal += duration;
        numberOfTests++;
    }
    double average = durationTotal/numberOfTests;
    System.out.println("-----------------------------------");
    System.out.println("Average Duration = " + average + " ns = "
            + (int)average / 1000000 +" ms (Approximately)");


}

}

Цей клас перевіряє швидкість створення нового TestClass. Кожне тестування робить це 20 мільйонів разів, і є 50 тестів.

Ось TestClass:

 public class TestClass {
     int a1= 5;
     int a2= 5; 
     int a3= 5;
     int a4= 5; 
     int a5= 5;
     int a6= 5; 
     int a7= 5;
     int a8= 5; 
     int a9= 5;
     int a10= 5; 
     int a11= 5;
     int a12=5; 
     int a13= 5;
     int a14= 5; 
 }

Я запустив SpeedTestклас і врешті отримав таке:

 Average Duration = 8.9625E8 ns = 896 ms (Approximately)

Зараз я міняю ints на байти в TestClass і запускаю його знову. Ось результат:

 Average Duration = 6.94375E8 ns = 694 ms (Approximately)

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


4
Зверніть увагу, що цей орієнтир - це лише вимірювання витрат, пов’язаних із розподілом та побудовою, і лише у випадку класу з великою кількістю окремих полів. Якщо операції арифметики / оновлення виконувались на полях, результати @ meriton дозволяють припустити, що це byteможе бути >> повільніше <<, ніж int.
Stephen C

Правда, я мав би сформулювати це краще, щоб це пояснити.
WVrock

2

байт, як правило, вважається 8 бітами. коротким зазвичай вважається 16 біт.

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

Однак ваш комп'ютер, ймовірно, не 8-бітний, і, ймовірно, не 16-бітний. це означає, що для отримання 16 або 8 бітів, зокрема, йому доведеться вдатися до "хитрощів", ​​які марно витрачають час, щоб зробити вигляд, що він має можливість отримати доступ до цих типів, коли це потрібно.

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


3
Я не впевнений, що ви маєте на увазі під "інженерною магією" ... більшість / усі сучасні процесори мають швидкі вказівки завантажувати байт і розширювати його підписом, зберігати його з реєстру повної ширини та робити ширину байта або арифметика короткої ширини в частині регістру повної ширини. Якби ви мали рацію, було б доцільно, де це можливо, замінити всі ints на longs на 64-бітному процесорі.
Ед Стауб,

Я можу уявити, що це правда. Я просто пам’ятаю, що в використовуваному нами симуляторі Motorola 68k більшість операцій могли працювати з 16-бітовими значеннями, а не з 32-бітними та 64-бітними. Я думав, що це означає, що системи мають бажаний розмір значення, який вони можуть отримати оптимально. Хоча я можу уявити, що сучасні 64-бітні процесори можуть з однаковою легкістю отримувати 8-бітні, 16-бітні, 32-бітні та 64-бітні, в цьому випадку це не питання. Дякую, що вказали на це.
Дмитро

"... загалом вважається ..." - Насправді чітко, однозначно >> вказано <<, що саме такі розміри. На Java. І контекстом цього питання є Java.
Stephen C

Велика кількість процесорів навіть використовує однакову кількість циклів для обробки та доступу до даних, які не мають розміру слова, тому насправді не варто турбуватися, якщо ви не вимірюєте на певній JVM та платформі.
drrob

Я намагаюся говорити загалом. Тим не менш, я насправді не впевнений у стандарті Java щодо розміру байтів, але на даний момент я впевнений, що якщо якийсь єретик вирішить не 8-бітові байти, Java не захоче торкатися їх десятифутовим полюсом. Однак деякі процесори потребують багатобайтного вирівнювання, і якщо платформа Java їх підтримує, їй потрібно буде робити все повільніше, щоб вмістити справу з цими меншими типами, або магічно представляти їх із більшими поданнями, ніж ви вимагали. Це завжди віддає перевагу int перед іншими типами, оскільки воно завжди використовує улюблений розмір системи.
Дмитро

2

Однією з причин меншої продуктивності коротких / байтових / символів є відсутність прямої підтримки для цих типів даних. Під прямою підтримкою це означає, що в специфікаціях JVM не згадується жоден набір інструкцій для цих типів даних. Інструкції, такі як зберігання, завантаження, додавання тощо мають версії для типу даних int. Але вони не мають версій для коротких / байт / символів. Наприклад, розглянемо нижче код Java:

void spin() {
 int i;
 for (i = 0; i < 100; i++) {
 ; // Loop body is empty
 }
}

Те саме перетворюється в машинний код, як показано нижче.

0 iconst_0 // Push int constant 0
1 istore_1 // Store into local variable 1 (i=0)
2 goto 8 // First time through don't increment
5 iinc 1 1 // Increment local variable 1 by 1 (i++)
8 iload_1 // Push local variable 1 (i)
9 bipush 100 // Push int constant 100
11 if_icmplt 5 // Compare and loop if less than (i < 100)
14 return // Return void when done

Тепер розгляньте можливість змінити int на short, як показано нижче.

void sspin() {
 short i;
 for (i = 0; i < 100; i++) {
 ; // Loop body is empty
 }
}

Відповідний машинний код зміниться наступним чином:

0 iconst_0
1 istore_1
2 goto 10
5 iload_1 // The short is treated as though an int
6 iconst_1
7 iadd
8 i2s // Truncate int to short
9 istore_1
10 iload_1
11 bipush 100
13 if_icmplt 5
16 return

Як ви можете помітити, для маніпулювання коротким типом даних він все ще використовує версію інструкції типу даних int і явно перетворює int у short, коли це потрібно. Тепер завдяки цьому продуктивність знижується.

Причина, за якою не надається безпосередня підтримка, така:

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

Цитується із специфікації JVM, яка присутня тут (Сторінка 58).


Це розібрані байт-коди; тобто JVM віртуальні інструкції . Вони не оптимізовані javacкомпілятором, і ви не можете зробити з них надійних висновків про те, як програма працюватиме в реальному житті. Компілятор JIT компілює ці байт-коди до фактичних машинних інструкцій і виконує досить серйозну оптимізацію в процесі. Якщо ви хочете проаналізувати ефективність коду, вам слід вивчити інструкції власного коду. (А це складно, тому що потрібно враховувати хронологічну поведінку багатоступеневого конвеєра x86_64.)
Стівен С,

Я вважаю, що специфікації Java призначені для реалізації реалізаторів javac. Тому я не думаю, що на цьому рівні проводиться оптимізація. У будь-якому випадку, я також можу бути абсолютно помилковим. Будь ласка, надішліть посилання для підтвердження свого висловлювання.
Manish Bansal

Ну ось один факт підтверджує моє твердження. Ви не знайдете жодних (достовірних) показників часу, які б розповідали, скільки тактових циклів бере кожна інструкція байт-коду JVM. Звичайно, не публікується Oracle або іншими постачальниками JVM. Також прочитайте stackoverflow.com/questions/1397009
Стівен С

Я знайшов стару (2008 р.) Статтю, де хтось намагався розробити незалежну від платформи модель для прогнозування продуктивності послідовностей байт-кодів. Вони стверджують, що їх прогнози були знижені на 25% порівняно з вимірами RDTSC .... на Pentium. І вони запускали JVM із вимкненою компіляцією JIT! Довідково: sciencedirect.com/science/article/pii/S1571066108004581
Стівен С

Я тут просто розгублений. Хіба моя відповідь не підтверджує факти, які ви виклали в розділі про перегляд?
Manish Bansal,

0

Різниця навряд чи помітна! Це більше питання дизайну, доречності, одноманітності, звичок тощо ... Іноді це лише питання смаку. Коли все, що вас турбує, це те, що ваша програма запускається, а заміна a floatна intне шкодить правильності, я не бачу жодної переваги в тому, щоб піти на те чи інше, якщо ви не зможете продемонструвати, що використання будь-якого типу змінює продуктивність. Налаштування продуктивності на основі типів, що відрізняються в 2 або 3 байти, - це справді останнє, про що вам слід дбати; Дональд Кнут якось сказав: "Передчасна оптимізація - корінь усього зла" (не впевнений, що це був він, відредагуйте, якщо у вас є відповідь).


5
Nit: A float не може представляти всі цілі числа intcan; також не може intпредставляти будь-яке неціле значення, яке floatможе. Тобто, хоча всі значення int є підмножиною довгих значень, int не є підмножиною float, а float не є підмножиною int.

Я сподіваюся, що відповідач мав намір писати substituting a float for a double, якщо відповідач повинен редагувати відповідь. Якщо не відповідач, повинен повісити голову від сорому і повернутися до основ з причин, описаних @pst, та з багатьох інших причин.
High Performance Mark

@HighPerformanceMark Ні, я ставлю int і float, тому що саме про це я думав. Моя відповідь не стосується Java, хоча я думав, що C ... Це має бути загальним. Підлий коментар, який ви там отримали.
mrk
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.