Changeset 18777 in josm for trunk


Ignore:
Timestamp:
2023-07-20T22:21:29+02:00 (13 months ago)
Author:
taylor.smock
Message:

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.

Location:
trunk
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/org/openstreetmap/josm/data/imagery/Shape.java

    r18360 r18777  
    88import java.util.AbstractList;
    99import java.util.List;
    10 import java.util.Objects;
    1110import java.util.stream.Collectors;
    12 import java.util.stream.Stream;
    1311
    1412import org.openstreetmap.gui.jmapviewer.Coordinate;
     
    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    }
     
    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
     
    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
     
    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
     
    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}
  • trunk/test/unit/org/openstreetmap/josm/data/imagery/ShapeTest.java

    r18341 r18777  
    55import static org.junit.jupiter.api.Assertions.assertEquals;
    66
     7import java.awt.Polygon;
     8import java.util.ArrayList;
    79import java.util.Arrays;
     10import java.util.Objects;
    811
     12import nl.jqno.equalsverifier.EqualsVerifier;
    913import org.junit.jupiter.api.Test;
    1014import org.junit.jupiter.params.ParameterizedTest;
     
    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}
Note: See TracChangeset for help on using the changeset viewer.