Skip to content
This repository has been archived by the owner on Oct 1, 2022. It is now read-only.

Use hash map to store settler images instead of 4-dim array #733

Open
wants to merge 7 commits into
base: master
Choose a base branch
from

Conversation

ewaldbenes
Copy link
Contributor

No description provided.

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);
Copy link
Member

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)?

Copy link
Member

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).

Copy link
Contributor Author

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.

Copy link
Member

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).

Copy link
Contributor Author

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() {
Copy link
Member

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 {
Copy link
Member

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) {
Copy link
Member

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;
Copy link
Member

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
Copy link
Member

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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use tabs as indent.

@andreas-eberle
Copy link
Member

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;
Copy link
Member

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,
Copy link
Member

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 {
Copy link
Member

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),
Copy link
Member

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.

Copy link
Contributor Author

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?

Copy link
Member

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).

Copy link
Contributor Author

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<>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

private

Copy link
Contributor Author

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);
Copy link
Member

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.

Copy link
Contributor Author

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);
Copy link
Member

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().

Copy link
Contributor Author

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.

@andreas-eberle
Copy link
Member

@michaelzangl: Can you have a look at the updated PR?

Copy link
Member

@michaelzangl michaelzangl left a 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) {
Copy link
Member

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;
Copy link
Member

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.

Copy link
Contributor Author

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);
Copy link
Member

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.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants