-
Notifications
You must be signed in to change notification settings - Fork 101
Use hash map to store settler images instead of 4-dim array #733
base: master
Are you sure you want to change the base?
Use hash map to store settler images instead of 4-dim array #733
Conversation
private SettlerImageMapItem getMapItem(EMovableType movableType, EMovableAction action, EMaterialType material, EDirection direction) { | ||
SettlerImageMapItem item = this.map[movableType.ordinal()][action.ordinal()][material.ordinal][direction.ordinal]; | ||
private SettlerImageMapItem getMapItem(SettlerImageFlavor settlerImageFlavor) { | ||
SettlerImageMapItem item = map.get(settlerImageFlavor); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did you time the old and the new retrieval? I guess the performance of accessing a hash map vs. accessing a 4 dim array might be worse. How much is it for a larger number of calls (e.g. 1000 or so)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, accessing a hash map should be faster than a 4 dimensional array (fewer memory accesses / cache misses).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Access of hash map by key is 0(1) so the same as the existing index lookup.
Memory footprint is far less because the array was as big as all possible combinations. I measured over a million. By using a hash map it went down to a little more than 1000. The expressiveness and the lower memory footprint were the 2 major reason why I did this refactoring.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually the HashMap implementation of java has no guaranteed access of O(1) but a worst case access of O(n) instead ;-). But we don't care about worst case complexity here, we care about actual performance. Measuring this is hard - you cannot measure the GC delays you get in a few frames so easily. So you need to track speed and GC consumption with both the old and new methods in a reproducible setting (should be enough to test it on PC though, Android is even more complex for this).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You said that the HashMap should be faster than the array. I'll look again at the hash function inside the flavor class and take care about object creation inside drawing methods to avoid GC pressure. Your two comments seem to contradict?
The array removal was merely a read- and maintainability change plus a huge memory gain.
|
||
public void loadFromMoveablesTextFile() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo "Movables" instead of "Mov[e]ables"
} | ||
|
||
private void readFromFile(InputStream file, int[][][][] priorities) throws IOException { | ||
private void readFromStream(InputStream file) throws IOException { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method can be inlined. Both, the caller and this method, are now two-liners.
@@ -182,18 +168,18 @@ private EDirection parseDirection(final String directionString) { | |||
return direction; | |||
} | |||
|
|||
private int calculatePriority(EMovableType type, EMovableAction action, EMaterialType material, EDirection direction) { | |||
private int calculatePriority(SettlerImageFlavor settlerImageFlavor) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Move this method to SettlerImageFlavor, since it only accesses fields of the passed object.
public final class SettlerImageFlavor { | ||
|
||
public static final SettlerImageFlavor NONE = new SettlerImageFlavor(null, null, null, null); | ||
private final EMovableType type; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use tabs as indents.
@@ -51,7 +53,8 @@ | |||
* | |||
* @author michael | |||
*/ | |||
public final class ImageProvider { | |||
// TODO: removed final for testing. introduce it again when some supertype could be extracted |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove the todo. The final isn't needed.
return Objects.hash(file, sequenceIndex, start, duration); | ||
} | ||
|
||
public int imageIndex(float progress) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use tabs as indent.
I saw that the jsettlers.graphics:test wasn't executed by travis and added this. |
@@ -0,0 +1,73 @@ | |||
package jsettlers.graphics.map.draw.settlerimages; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add the copyright notice.
public class SettlerImageMapTest { | ||
|
||
private static final SettlerImageFlavor TEST_IMAGE_FLAVOR = new SettlerImageFlavor( | ||
EMovableType.BEARER, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use tabs and use one line here only. We have a line length of 200 characters.
} | ||
|
||
@Test | ||
public void givenTextWithTestEntry_whenLoad_thenContainsTestEntry() throws IOException { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please don't use _
in method names. Java convention expects camel case only.
EMovableAction.NO_ACTION, EMaterialType.NO_MATERIAL, | ||
EDirection.SOUTH_EAST, 0); | ||
Image image = SettlerImageMap.getInstance().getImageForSettler( | ||
new SettlerImageFlavor(type, EMovableAction.NO_ACTION, EMaterialType.NO_MATERIAL, EDirection.SOUTH_EAST), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the main problem I see with your approach (and the reason why we did it the old way):
You are creating an object here that the JIT cannot (or at least in my tests with Android could not) inline. Mind that Android seems to be far worse at optimizing code than the normal Java JIT is.
This will make the young generation grow and thus create a lot of GC activity, since you might have thousands of settlers on one screen, especially when zooming out.
This is the reason why the drawing code attempts not to create any objects during painting phases.
We should test this and - if it is a problem - use a long instead of the SettlerImageFlavor (you can then create a static method that generates that unique long from the given parameters). Then use a library that supports long hash maps.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right. I have overseen that fact. Maybe I can find a way to cache the SettlerImageFlavor instead of using a long since it would be the same approach. What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The usual way to cache such objects is to put them in a hash table - You have a chicken and egg problem then ;-).
Generating the long is no big deal and should be pretty fast. A long -> Object hash map is probably the fastest you can get (in this case probably ~10 times faster than the way you are using now - at least that is my guess from my experience, might be more or less).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've looked at this again. The SettlerImageFlavor can be created once in the constructor and the overhead is simply gone.
|
||
private final SettlerImageMapItem[][][][] map; | ||
final HashMap<SettlerImageFlavor, SettlerImageMapItem> map = new HashMap<>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
private
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's used to assert contents in the map in the class test.
|
||
@Override | ||
public int hashCode() { | ||
return Objects.hash(file, sequenceIndex, start, duration); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same allocation problem here - use direct hash code computation instead to avoid allocating an array.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This object is used as a value in the hash map so no hashing is done. I don't know why I added hashCode and equals here ^^
|
||
@Override | ||
public int hashCode() { | ||
return Objects.hash(type, action, material, direction); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ewaldbenes Then my comment should have been placed here - same problem, object allocation in hashCode()
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now I understand. I will take care of object allocations hashing and drawing methods.
a91311e
to
ae03171
Compare
Convert indents from space to tabs. Fix typos.
…nstance constant.
@michaelzangl: Can you have a look at the updated PR? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks fine now. Code quality has improved a lot compared to the tings I hacked...
I would be really interested in speed impacts, but I estimate it to be not so much of an issue and that the performance should be about the same.
addEntryToMap(priorities, type, action, material, direction, new SettlerImageMapItem(fileIndex, sequence, start, duration), priority); | ||
SettlerImageFlavor flavor = new SettlerImageFlavor(type, action, material, direction); | ||
int priority = flavor.calculatePriority(); | ||
if(!priorities.containsKey(flavor) || priorities.get(flavor) < priority) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can make this faster by using:
var value = priorities.get(flavor);
if (value != null || value ...) {}
private final EDirection direction; | ||
|
||
public SettlerImageFlavor(EMovableType type, EMovableAction action, EMaterialType material, EDirection direction) { | ||
this.type = type; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can ensure that the parameters are not null by adding Objects.requireNonNull() calls.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The map contains one default entry that is defined by all parameters as null and comes from "movables.txt" file, entry ",,,=10, 0, 0, 1". * is converted to null. That's why I cannot prohibit null.
@Override | ||
public int hashCode() { | ||
int result = type != null ? type.hashCode() : 0; | ||
result = 31 * result + (action != null ? action.hashCode() : 0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here - java is very efficient at testing for non-null and throwing an exception if a value is null.
4d10390
to
1a726f5
Compare
No description provided.