Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

8345668: ZoneOffset.ofTotalSeconds performance regression #22854

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions src/java.base/share/classes/java/time/ZoneOffset.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2012, 2023, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2012, 2024, Oracle and/or its affiliates. All rights reserved.
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't be 2025 too?

Copy link
Member Author

Choose a reason for hiding this comment

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

This PR was published last year and ZoneOffset has not changed since then. So I think 2024 is fine

* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
Expand Down Expand Up @@ -424,11 +424,17 @@ public static ZoneOffset ofTotalSeconds(int totalSeconds) {
throw new DateTimeException("Zone offset not in valid range: -18:00 to +18:00");
}
if (totalSeconds % (15 * SECONDS_PER_MINUTE) == 0) {
return SECONDS_CACHE.computeIfAbsent(totalSeconds, totalSecs -> {
ZoneOffset result = new ZoneOffset(totalSecs);
Integer totalSecs = totalSeconds;
ZoneOffset result = SECONDS_CACHE.get(totalSecs);
Copy link
Contributor

Choose a reason for hiding this comment

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

Here, each call may allocate an Integer object. The maximum number of ZoneOffsets that need to be cached here is only 148. Using AtomicReferenceArray is better than AtomicConcurrentHashMap.

Copy link
Contributor

@wenshao wenshao Dec 20, 2024

Choose a reason for hiding this comment

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

For example:

static final AtomicReferenceArray<ZoneOffset> MINUTES_15_CACHE = new AtomicReferenceArray<>(37 * 4);

    public static ZoneOffset ofTotalSeconds(int totalSeconds) {
        // ...
        int minutes15Rem = totalSeconds / (15 * SECONDS_PER_MINUTE);
        if (totalSeconds - minutes15Rem * 15 * SECONDS_PER_MINUTE == 0) {
            int cacheIndex = minutes15Rem + 18 * 4;
            ZoneOffset result = MINUTES_15_CACHE.get(cacheIndex);
            if (result == null) {
                result = new ZoneOffset(totalSeconds);
                if (!MINUTES_15_CACHE.compareAndSet(cacheIndex, null, result)) {
                    result = MINUTES_15_CACHE.get(minutes15Rem);
                }
            }
            return result;
        }
       // ...
    }

Copy link
Member Author

@naotoj naotoj Dec 20, 2024

Choose a reason for hiding this comment

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

Hi Shaojin,
Thanks for the suggestion, but I am not planning to improve the code more than backing out the offending fix at this time. (btw, cache size would be 149 as 18:00 and -18:00 are inclusive)

Copy link
Contributor

Choose a reason for hiding this comment

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

Can I submit a PR to make this improvement?

Copy link
Member

@liach liach Dec 21, 2024

Choose a reason for hiding this comment

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

@wenshao I agree with your proposal. Also for this part:

ZoneOffset result = MINUTES_15_CACHE.get(cacheIndex);
if (result == null) {
    result = new ZoneOffset(totalSeconds);
    if (!MINUTES_15_CACHE.compareAndSet(cacheIndex, null, result)) {
        result = MINUTES_15_CACHE.get(minutes15Rem);
    }
}

I recommend a rewrite:

ZoneOffset result = MINUTES_15_CACHE.getPlain(cacheIndex);
if (result == null) {
    result = new ZoneOffset(totalSeconds);
    ZoneOffset existing = MINUTES_15_CACHE.compareAndExchange(cacheIndex, null, result);
    return existing == null ? result : existing;
}

The getPlain is safe because ZoneOffset is thread safe, so you can use the object when you can observe a ZoneOffset object reference. Also compareAndExchange avoids extra operations if we failed to racily set the computed ZoneOffset.

if (result == null) {
result = new ZoneOffset(totalSeconds);
var existing = SECONDS_CACHE.putIfAbsent(totalSecs, result);
if (existing != null) {
result = existing;
}
ID_CACHE.putIfAbsent(result.getId(), result);
return result;
});
}
return result;
} else {
return new ZoneOffset(totalSeconds);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2012, 2022, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
Expand Down Expand Up @@ -309,7 +309,15 @@ public Iterator<Entry<String, Long>> getTextIterator(Chronology chrono, Temporal

private Object findStore(TemporalField field, Locale locale) {
Entry<TemporalField, Locale> key = createEntry(field, locale);
return CACHE.computeIfAbsent(key, e -> createStore(e.getKey(), e.getValue()));
Object store = CACHE.get(key);
if (store == null) {
store = createStore(field, locale);
var existing = CACHE.putIfAbsent(key, store);
if (existing != null) {
store = existing;
}
}
return store;
}

private static int toWeekDay(int calWeekDay) {
Expand Down
12 changes: 10 additions & 2 deletions src/java.base/share/classes/java/time/format/DecimalStyle.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2012, 2022, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
Expand Down Expand Up @@ -160,7 +160,15 @@ public static DecimalStyle ofDefaultLocale() {
*/
public static DecimalStyle of(Locale locale) {
Objects.requireNonNull(locale, "locale");
return CACHE.computeIfAbsent(locale, DecimalStyle::create);
DecimalStyle info = CACHE.get(locale);
if (info == null) {
info = create(locale);
var existing = CACHE.putIfAbsent(locale, info);
if (existing != null) {
info = existing;
}
}
return info;
}

private static DecimalStyle create(Locale locale) {
Expand Down