В java более эффективно использовать byte или short вместо int и float вместо double?

91

Я заметил, что всегда использую int и double, независимо от того, насколько маленьким или большим должно быть число. Итак, в java более эффективно использовать byteили shortвместо intи floatвместо double?

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

Я знаю, что в Java нет беззнаковых типов, но могу ли я что-нибудь сделать, если бы знал, что число будет только положительным?

Под эффективным я в основном имею в виду обработку. Я бы предположил, что сборщик мусора был бы намного быстрее, если бы все переменные были половинными, и эти вычисления, вероятно, тоже были бы немного быстрее. (Думаю, поскольку я работаю над Android, мне тоже нужно немного побеспокоиться о баране)

(Я бы предположил, что сборщик мусора работает только с объектами, а не с примитивами, но все же удаляет все примитивы в заброшенных объектах, верно?)

Я попробовал это с небольшим приложением для Android, которое у меня есть, но вообще не заметил разницы. (Хотя я ничего «научно» не измерял.)

Я ошибаюсь, полагая, что это должно быть быстрее и эффективнее? Я бы не хотел пройти через все и изменить все в масштабной программе, чтобы узнать, что я зря потратил свое время.

Стоит ли делать это с самого начала, когда я начинаю новый проект? (Я имею в виду, я думаю, что каждая мелочь могла бы помочь, но опять же, если так, то почему кажется, что никто не делает это.)

ДисибиоАарон
источник

Ответы:

109

Я ошибаюсь, полагая, что это должно быть быстрее и эффективнее? Я бы не хотел пройти через все и изменить все в масштабной программе, чтобы узнать, что я зря потратил свое время.

Короткий ответ

Да ты ошибаешься. В большинстве случаев это не имеет большого значения с точки зрения используемого пространства.

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

Более длинный ответ

Виртуальная машина 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 - Я уверен , что макеты объектов будут определены во время загрузки класса, и они не изменяются после этого. См. «Мелкий шрифт» в моем ответе. Если фактические схемы памяти изменились, когда код был JITed, 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]может быть больше или менее эффективен, чем an int[2], но не настолько, чтобы иметь значение в любом случае), но отдельные значения более эффективно сохраняются как int.
supercat
2
Что я проверил: эти тесты всегда использовали int ('3') как фактор или операнд присваивания (вариант цикла, затем приведенный). Я использовал типизированные факторы / операнды присваивания в зависимости от типа lvalue: int mult 76,481 нс int mult (типизированный) 72,581 нс короткий mult 87,908 нс короткий mult (типизированный) 90,772 нс байт mult 87,859 нс байт mult (типизированный) 89,524 нс int [] trav 88,905 нс int [] trav (типизированный) 89,126 нс короткий [] trav 10,563 нс короткий [] trav (типизированный) 10,039 нс байт [] trav 8,356 нс байт [] trav (типизированный) 8,338 нс Я полагаю, что есть много лишнего литья. эти тесты проводились на вкладке Android.
Bondax
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)

Теперь я меняю целые числа на байты в TestClass и снова запускаю его. Вот результат:

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

Я считаю, что этот эксперимент показывает, что если вы создаете экземпляры огромного количества переменных, использование byte вместо int может повысить эффективность.

WVrock
источник
4
Обратите внимание, что этот эталонный тест измеряет только затраты, связанные с размещением и построением, и только в случае класса с множеством отдельных полей. Если с полями выполнялись арифметические операции / операции обновления, результаты @meriton предполагают, что это byteмогло быть >> медленнее <<, чем int.
Stephen C
Правда, мне следовало сформулировать это получше, чтобы прояснить.
WVrock
2

байт обычно считается 8-битным. short обычно считается 16-битным.

В «чистой» среде, которая не является java, поскольку вся реализация байтов, длинных, коротких и других забавных вещей обычно скрыта от вас, byte лучше использует пространство.

Однако ваш компьютер, вероятно, не 8-битный, и, вероятно, не 16-битный. это означает, что, в частности, для получения 16 или 8 битов потребуется прибегнуть к «хитрости», которая тратит время, чтобы притвориться, что у него есть возможность обращаться к этим типам, когда это необходимо.

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

Дмитрий
источник
3
Я не уверен, что вы имеете в виду под "инженерной магией" ... у большинства / всех современных процессоров есть быстрые инструкции для загрузки байта и расширения его по знаку, для сохранения одного из регистра полной ширины и для выполнения байтовой ширины или арифметика малой ширины в части регистра полной ширины. Если бы вы были правы, было бы разумно, где это возможно, заменить все int на long на 64-битном процессоре.
Эд Стауб
Я могу представить, что это правда. Я просто помню, что в симуляторе Motorola 68k, который мы использовали, большинство операций могло работать с 16-битными значениями, а не с 32-битными или 64-битными. Я думал, что это означает, что у системы есть предпочтительный размер значения, который она может получить оптимальным образом. Хотя я могу представить, что современные 64-битные процессоры могут с одинаковой легкостью получать 8-битные, 16-битные, 32-битные и 64-битные версии, в данном случае это не проблема. Спасибо что подметил это.
Дмитрий
«... принято считать ...» - на самом деле ясно, недвусмысленно >> указаны << именно такие размеры. В Java. И контекст этого вопроса - Java.
Stephen C
Большое количество процессоров даже используют одинаковое количество циклов для манипулирования данными, не имеющими размера слова, и доступа к ним, поэтому не стоит беспокоиться, если вы не проводите измерения на конкретной JVM и платформе.
drrob
Я пытаюсь говорить в целом. Тем не менее, я не совсем уверен в стандарте Java в отношении размера байта, но на данный момент я почти убежден, что если какой-либо еретик решит, что байты не 8 бит, Java не захочет прикасаться к ним десятифутовым шестом. Однако для некоторых процессоров требуется многобайтовое выравнивание, и, если платформа Java их поддерживает, придется делать что-то медленнее, чтобы приспособиться к работе с этими меньшими типами, или волшебным образом представлять их в более крупных представлениях, чем вы запрашивали. Они всегда предпочитают int другим типам, так как всегда используют любимый размер системы.
Дмитрий
2

Одна из причин меньшей производительности short / byte / char заключается в отсутствии прямой поддержки этих типов данных. Под прямой поддержкой это означает, что в спецификациях JVM не упоминается какой-либо набор инструкций для этих типов данных. Такие инструкции, как store, load, add и т. Д., Имеют версии для типа данных int. Но у них нет версий для short / byte / char. Например, рассмотрим ниже 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
Stephen C
Я нашел старую (2008 г.) статью, в которой кто-то пытался разработать платформо-независимую модель для прогнозирования производительности последовательностей байт-кода. Они утверждают, что их прогнозы были ошибочными на 25% по сравнению с измерениями RDTSC ... на Pentium. И они запускали JVM с отключенной компиляцией JIT! Ссылка: sciencedirect.com/science/article/pii/S1571066108004581
Стивен С.
Я здесь просто запутался. Разве мой ответ не подтверждает факты, которые вы указали в разделе повторного просмотра?
Manish Bansal
0

Разница практически не заметна! Это скорее вопрос дизайна, соответствия, единообразия, привычки и т. Д. Иногда это просто вопрос вкуса. Когда все, что вас волнует, это то, что ваша программа запускается и работает, и замена a floatна a intне повредит правильности, я не вижу преимуществ в выборе того или другого, если вы не можете продемонстрировать, что использование любого типа изменяет производительность. Настройка производительности на основе типов, которые различаются 2 или 3 байтами, - это последнее, о чем вам следует заботиться; Дональд Кнут однажды сказал: «Преждевременная оптимизация - корень всех зол» (не уверен, что это был он, отредактируйте, если у вас есть ответ).

MRK
источник
5
Nit: A float не может представлять все целые числа в intбанке; и не может intпредставлять любое нецелое значение, которое floatможет. То есть, хотя все значения int являются подмножеством длинных значений, int не является подмножеством float, а float не является подмножеством int.
Я ожидаю, что ответчик хотел написать substituting a float for a double, если это так, ответчик должен отредактировать ответ. Если он не отвечает, он должен от стыда опустить голову и вернуться к основам по причинам, указанным в @pst, и по многим другим причинам.
High Performance Mark
@HighPerformanceMark Нет, я поставил int и float, потому что это то, о чем я думал. Мой ответ не относится к Java, хотя я думал о C ... Он должен быть общим. Подлый комментарий у вас там есть.
mrk