EnumSet. Первое, что мне про него было интересно — это, конечно, скаляризация итератора. Ну, потому что по любой коллекции очень удобно ходить циклом foreach, но не всегда хочется за это удобство платить, пусть даже и не дорого: public static enum Enum3 {
FIRST,
SECOND,
THIRD
}
...
final EnumSet set = EnumSet.allOf( Enum3.class );
...
//benchmark start
long sum = 0;
for( final Enum e : set ) {
sum += e.ordinal();
}
return sum;
//benchmark end
... JIT меня здесь не подвел: как и в большинстве коллекций, которые я до сих пор исследовал, итератор успешно скаляризуется. По-крайней мере, пока количество элементов в перечислении не превышает 64-х. Я не сумел сходу понять, что там такого в
JumboEnumSet.EnumSetIterator-е, что смущает скаляризацию, а копать глубже мне немного лениво, потому что ДА ГОСПОДИ ИИСУСЕ КТО Ж ДЕЛАЕТ ПЕРЕЧИСЛЕНИЯ ПО 64+ ЭЛЕМЕНТОВ? Да таких случаев один на 1000, хрен с ней тогда, со скаляризацией, тут о вечном думать пора... А вот более практичный вопрос: скаляризуется ли сам
EnumSet в примере выше? То есть: если внести создание EnumSet.allOf(...) в бенчмарк — сумеет ли JIT спроецировать этот EnumSet в простой скалярный long? Простой тест показывает, что нет. Но, конечно, гораздо интереснее понять — почему. По первости я грешил на вот этот метод:
public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
Enum[] universe = getUniverse(elementType);
if (universe == null)
throw new ClassCastException(elementType + " not an enum");
if (universe.length <= 64)
return new RegularEnumSet<>(elementType, universe);
else
return new JumboEnumSet<>(elementType, universe);
}
мне казалось довольно логичным, что статически предсказать ветку RegularEnumSet/JumboEnumSet невозможно, и получается уже известный сценарий с merge points. Чтобы это проверить я скопировал весь исходный код EnumSet/RegularEnumSet/JumboEnumSet из java.util к себе, и закомментировал ветку с JumboEnumSet. На удивление, это не помогло, и я пошел копать дальше. После какого-то количества промежуточных экспериментов (я уже, было, начал писать свой собственный SimpleEnumSet — чтобы, начав с крайне простого кода, потом добавлять понемногу функционал, пока скаляризация не сломается) поиск сошелся вот на чем: RegularEnumSet.EnumSetIterator это не-статический внутренний класс. В этом, казалось бы, нет ничего плохого — если только вы не слушали мой доклад на JPoint, где я уже разбирал ровно такой же случай, только на примере Arrays.asList(..) ...Оказывается, из-за загадочного бага в JIT-е, скаляризация ломается (не всегда, но часто), если на потенциально скаляризуемый объект ссылается уже скаляризованный объект. То есть
EnumSetIterator успешно скаляризуется, но одно из его полей — неявная ссылка на объект родительского класса, на RegularEnumSet — застревает костью в горле у алгоритма скаляризации, когда этот алгоритм пытается скаляризовать сам RegularEnumSet. Другими словами, вот такой код... final EnumSet<Enum3> set = EnumSet.allOf( Enum3.class ); final boolean b = set.contains( Enum3.SECOND ); ...(без итератора) вполне успешно скаляризуется. Как и код с итератором (но без аллокации
EnumSet) в самом начале статьи. Проблемы возникают когда мы эти два куска объединяем: когда мы хотим, чтобы одновременно и итератор, и сам RegularEnumSet скаляризовались. Тут нас ждет облом.К сожалению, ситуация с
EnumSet несколько хуже, чем с Arrays.asList(). В последнем случае итератор был не-статическим классом просто по недосмотру — раньше это никого не волновало. Сделать его не статическим ничего особо не стоило, никакой функциональности это не мешало. Благодаря усилиям Тагира Валеева это изменение даже было принято патчем в OpenJDK, так что в 9-ке, вероятно, Arrays.asList() будет скаляризоваться без проблем. А вот с EnumSet такое простое решение не пройдет, итератор здесь не-статический не случайно: доступ к родительскому объекту ему необходим для реализации метода remove(). Остается надеяться только на то, что JDK-8155769 когда-нибудь починят. P.S. Удивительный факт, который остался немного за кадром: в этом коде
public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
Enum[] universe = getUniverse(elementType);
if (universe == null)
throw new ClassCastException(elementType + " not an enum");
if (universe.length <= 64)
return new RegularEnumSet<>(elementType, universe);
else
return new JumboEnumSet<>(elementType, universe);
}
JIT ухитряется как-то протащить реальный класс перечисления через getUniverse(..), и статически вычислить условие (universe.length <= 64)! Да они там совсем укурились в своем hotspot-dev!P.P.S. Что меня в теме скаляризации вдохновляет сейчас: когда я начинал ее смотреть, я был готов к сценарию, что тут кромешный адъ и тьма египетская, и без знания наизусть всех кишок компилятора разобраться, или предсказать, или починить скаляризацию невозможно. А оказалось, что странности есть, но пока что все они умещаются в очень небольшой список шаблонов. То есть достаточно помнить по пальцам считанное число граблей (пусть даже не про все из них понятно, откуда они взялись), да знать ключики типа
-XX:+PrintCompilation/-XX:+PrintInlining, и в большинстве случаев скаляризацию удается заставить работать, либо обозначить ключевые причины, почему она в таком коде работать и не будет. Пока, по крайней мере, все выглядит так.
А все из какой-то реальной задачи ( упаси господи ) или же "простое" академическое исследование ?
ОтветитьУдалитьПочему сразу "упаси господи"? У нас в коде испокон веков флаги шли битовой маской, я, конечно, под это дело завел enum, и где можно пользовался EnumSet/EnumMap, потому как очевидно удобнее же. Вот и стало интересно. Конечно, код не ровно так выглядит -- примеры-то я "академические" стараюсь делать.
УдалитьИли вы про то, действительно ли это отсутствие скаляризации EnumSet является где-то перфомансной проблемой? Тогда нет, пока я таких примеров, где это является проблемой, у себя не видел.
Как ни странно, мне до сих пор битовая маска часто удобнее, чем EnumSet. Как минимум потому, что она value-type, то есть не надо следить, что утечёт ссылка в недоверенный код и там кто-то мои флаги поменяет. Делать явно копии или оборачивать везде в unmodifiableSet как-то мутно.
УдалитьРуслан, я давно хотел тебя спросить, не в Word'е ли ты пишешь посты. Но теперь я, кажется, знаю ответ: по-моему, даже Word не растягивает слово "код" на целую строчку
ОтветитьУдалитьЯ пишу прямо в блогспотовском редакторе, и ручками расставляю тэги и длинные тире. Только никому не говори :)
УдалитьВ упор не вижу растянутого кода. Ты, наверное, какой-то мобильный браузер используешь?
Я в IE читаю, только никому не говори :-). Сейчас пришлю скриншот
УдалитьНе пробовал ли патчить openjdk на тему поведения EA - не исключено, что в некоторых случаях патч не велик будет? Верифицировать и протолкнуть это уже проторенная дорожка Тагиром
ОтветитьУдалитьВ смысле -- патчить OpenJDK? Я не знаю, как починить JDK-8155769 -- даже толком не понимаю, что там сломано. А на уровне джава-кода починить не получится, я же пишу -- здесь-то итератор должен иметь ссылку на EnumSet.
Удалитьнет, не на уровне java, а на уровне jvm - почему бы не пуститься во все тяжкие...
УдалитьЭто ж на С++ придется писать. Боюсь, потом долго грехи замаливать придется :)
УдалитьМорально не готов пока к таким экспериментам над собой.
Общался тут давеча с С++-никами
ОтветитьУдалитьУ них можно и на стеке аллоцировать и в хипе - просто указав это языку )))
Ляпота)))
Сорри за небольшой неконструктив - навеяло )
как нечего делать
УдалитьFoo foo(); // on stack
Foo *foo = new Foo(); // in heap
и можно еще передавать значения по ссылке, а можно по значению - это значит, что каждый раз на вызов метода будет создан новый объект через copy-constructor