Skip to content

Commit 4f48049

Browse files
author
taylor.smock
committed
Reduce allocations during startup from Shape
Shape#hashCode made between 8 and 24 MB of allocations at startup. This was reduced by moving the math out of Arrays#hash and the default List#hash methods. This additionally reduced the allocations from the Shape#equals method. As an implementation note, the Shape#hashCode method returns exactly the same hashcode as it did prior to this change. This may change in the future. git-svn-id: https://josm.openstreetmap.de/svn/trunk@18777 0c6e7542-c601-0410-84e7-c038aed88b3b
1 parent 35e1c5f commit 4f48049

File tree

2 files changed

+111
-19
lines changed

2 files changed

+111
-19
lines changed

src/org/openstreetmap/josm/data/imagery/Shape.java

Lines changed: 86 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,7 @@
77
import java.text.MessageFormat;
88
import java.util.AbstractList;
99
import java.util.List;
10-
import java.util.Objects;
1110
import java.util.stream.Collectors;
12-
import java.util.stream.Stream;
1311

1412
import org.openstreetmap.gui.jmapviewer.Coordinate;
1513
import org.openstreetmap.josm.data.coor.LatLon;
@@ -53,8 +51,7 @@ public Shape() {
5351
*/
5452
public String encodeAsString(String separator) {
5553
return getPoints().stream()
56-
.flatMap(c -> Stream.of(c.getLat(), c.getLon()))
57-
.map(String::valueOf)
54+
.map(c -> c.getLat() + separator + c.getLon())
5855
.collect(Collectors.joining(separator));
5956
}
6057

@@ -70,19 +67,7 @@ public static String encodeAsString(List<Shape> shapes) {
7067
}
7168

7269
public List<Coordinate> getPoints() {
73-
return new AbstractList<Coordinate>() {
74-
@Override
75-
public Coordinate get(int index) {
76-
double lat = coords.ypoints[index] / LatLon.MAX_SERVER_INV_PRECISION;
77-
double lon = coords.xpoints[index] / LatLon.MAX_SERVER_INV_PRECISION;
78-
return new Coordinate(lat, lon);
79-
}
80-
81-
@Override
82-
public int size() {
83-
return coords.npoints;
84-
}
85-
};
70+
return new CoordinateList(this.coords);
8671
}
8772

8873
/**
@@ -126,19 +111,101 @@ public void addPoint(String sLat, String sLon) {
126111

127112
@Override
128113
public int hashCode() {
129-
return Objects.hash(getPoints());
114+
// This was Objects.hash(getPoints()), but that made 8-24MB of allocations on application startup
115+
return 31 + hashPolygon(this.coords); // Arrays#hash, new array instantiation
130116
}
131117

132118
@Override
133119
public boolean equals(Object obj) {
134120
if (this == obj) return true;
135121
if (obj == null || getClass() != obj.getClass()) return false;
136122
Shape shape = (Shape) obj;
137-
return Objects.equals(getPoints(), shape.getPoints());
123+
return equalsPolygon(this.coords, shape.coords);
138124
}
139125

140126
@Override
141127
public String toString() {
142128
return "Shape{coords=" + getPoints() + '}';
143129
}
130+
131+
/**
132+
* Hash a polygon
133+
* @param coords The polygon to hash
134+
* @return The hashcode to use; equivalent to {@link Polygon#hashCode()}, but zero allocations.
135+
*/
136+
private static int hashPolygon(Polygon coords) {
137+
// This is faster than coords.hashCode() by ~90% and performs effectively no memory allocations.
138+
// This was originally written to replace Objects.hash(getPoints()). The only difference is +31 on the return.
139+
// Objects.hash(getPoints()) -> Arrays.hash(getPoints()) -> sum of hashes of Coordinates
140+
// First, AbstractList#hashCode equivalent
141+
int hashCode = 1;
142+
for (int index = 0; index < coords.npoints; index++) {
143+
final double lat = coords.ypoints[index] / LatLon.MAX_SERVER_INV_PRECISION;
144+
final double lon = coords.xpoints[index] / LatLon.MAX_SERVER_INV_PRECISION;
145+
// Coordinate uses Object.hash(x, y) - new array instantiation *and* two Double instantiations
146+
// Double conversion is 3.22MB
147+
// The array creation for Object.hash(x, y) is 2.11 MB
148+
final int coordinateHash = 31 * (31 + Double.hashCode(lon)) + Double.hashCode(lat);
149+
hashCode = 31 * hashCode + coordinateHash; // hashCode * 31 + coordinate.hashCode()
150+
}
151+
return hashCode;
152+
}
153+
154+
/**
155+
* Check that two {@link Polygon}s are equal
156+
* @param first The first polygon to check
157+
* @param second The second polygon to check
158+
* @return {@code true} if the polygons are equal
159+
*/
160+
private static boolean equalsPolygon(Polygon first, Polygon second) {
161+
// If the coordinate lists aren't the same size, short circuit.
162+
// We aren't doing fuzzy comparisons here.
163+
if (first.npoints != second.npoints) {
164+
return false;
165+
}
166+
for (int i = 0; i < first.npoints; i++) {
167+
if (first.xpoints[i] != second.xpoints[i] ||
168+
first.ypoints[i] != second.ypoints[i]) {
169+
return false;
170+
}
171+
}
172+
return true;
173+
}
174+
175+
/**
176+
* A list of {@link Coordinate}s that attempts to be very efficient in terms of CPU time and memory allocations.
177+
*/
178+
private static final class CoordinateList extends AbstractList<Coordinate> {
179+
private final Polygon coords;
180+
181+
CoordinateList(Polygon coords) {
182+
this.coords = coords;
183+
}
184+
185+
@Override
186+
public Coordinate get(int index) {
187+
double lat = coords.ypoints[index] / LatLon.MAX_SERVER_INV_PRECISION;
188+
double lon = coords.xpoints[index] / LatLon.MAX_SERVER_INV_PRECISION;
189+
return new Coordinate(lat, lon);
190+
}
191+
192+
@Override
193+
public int size() {
194+
return coords.npoints;
195+
}
196+
197+
@Override
198+
public boolean equals(Object o) {
199+
if (o instanceof CoordinateList) {
200+
CoordinateList other = (CoordinateList) o;
201+
return equalsPolygon(this.coords, other.coords);
202+
}
203+
return super.equals(o);
204+
}
205+
206+
@Override
207+
public int hashCode() {
208+
return hashPolygon(this.coords);
209+
}
210+
}
144211
}

test/unit/org/openstreetmap/josm/data/imagery/ShapeTest.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,12 @@
44
import static org.junit.jupiter.api.Assertions.assertAll;
55
import static org.junit.jupiter.api.Assertions.assertEquals;
66

7+
import java.awt.Polygon;
8+
import java.util.ArrayList;
79
import java.util.Arrays;
10+
import java.util.Objects;
811

12+
import nl.jqno.equalsverifier.EqualsVerifier;
913
import org.junit.jupiter.api.Test;
1014
import org.junit.jupiter.params.ParameterizedTest;
1115
import org.junit.jupiter.params.provider.ValueSource;
@@ -52,4 +56,25 @@ void testDoubleEdgeCases(final double coordinate) {
5256
() -> assertEquals(coordinate, shape.getPoints().get(0).getLon()),
5357
() -> assertEquals(coordinate, shape.getPoints().get(1).getLat()));
5458
}
59+
60+
/**
61+
* Ensure that the hashcode is semantically the same as what it previously was
62+
*/
63+
@ParameterizedTest
64+
@ValueSource(strings = {"47.1 11.1 47.2 11.2 47.3 11.3", "-47.1 -11.1 -47.2 -11.2 -47.3 -11.3"})
65+
void testHashCode(String shapeString) {
66+
final Shape shape = new Shape(shapeString, " ");
67+
assertEquals(Objects.hash(shape.getPoints()), shape.hashCode(),
68+
"The hashcode for shape should be the same as that for the point list (specific coord list)");
69+
assertEquals(Objects.hash(new ArrayList<>(shape.getPoints())), shape.hashCode(),
70+
"The hashcode for shape should be the same as that for the point list (non-specific)");
71+
}
72+
73+
@Test
74+
void testEqualsHashCodeContract() {
75+
EqualsVerifier.simple().forClass(Shape.class)
76+
.withNonnullFields("coords")
77+
.withPrefabValues(Polygon.class, new Polygon(), new Polygon(new int[] {1, 2, 3}, new int[]{4, 5, 6}, 3))
78+
.verify();
79+
}
5580
}

0 commit comments

Comments
 (0)