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

optimize performance #8

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
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
103 changes: 103 additions & 0 deletions src/main/java/com/aventrix/jnanoid/jnanoid/MathUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* Copyright (C) 2011 The Guava Authors
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.aventrix.jnanoid.jnanoid;

import java.math.RoundingMode;

/**
* Methods to perform fast log
*/
public final class MathUtils {

/**
* The biggest half power of two that can fit in an unsigned int.
*/
private static final int MAX_POWER_OF_SQRT2_UNSIGNED = 0xB504F333;

private MathUtils() {
throw new IllegalStateException();
}

/**
* Returns the base-2 logarithm of {@code x}, rounded according to the specified rounding mode.
*
* @throws IllegalArgumentException if {@code x <= 0}
* @throws ArithmeticException if {@code mode} is {@link RoundingMode#UNNECESSARY} and {@code x}
* is not a power of two
*/
@SuppressWarnings("fallthrough")
public static int log2(int x, RoundingMode mode) {
checkPositive("x", x);
switch (mode) {
case UNNECESSARY:
checkRoundingUnnecessary(isPowerOfTwo(x));
// fall through
case DOWN:
case FLOOR:
return (Integer.SIZE - 1) - Integer.numberOfLeadingZeros(x);

case UP:
case CEILING:
return Integer.SIZE - Integer.numberOfLeadingZeros(x - 1);

case HALF_DOWN:
case HALF_UP:
case HALF_EVEN:
// Since sqrt(2) is irrational, log2(x) - logFloor cannot be exactly 0.5
int leadingZeros = Integer.numberOfLeadingZeros(x);
int cmp = MAX_POWER_OF_SQRT2_UNSIGNED >>> leadingZeros;
// floor(2^(logFloor + 0.5))
int logFloor = (Integer.SIZE - 1) - leadingZeros;
return logFloor + lessThanBranchFree(cmp, x);

default:
throw new AssertionError();
}
}

private static void checkRoundingUnnecessary(boolean condition) {
if (!condition) {
throw new ArithmeticException("mode was UNNECESSARY, but rounding was necessary");
}
}

/**
* Returns {@code true} if {@code x} represents a power of two.
*
* <p>This differs from {@code Integer.bitCount(x) == 1}, because {@code
* Integer.bitCount(Integer.MIN_VALUE) == 1}, but {@link Integer#MIN_VALUE} is not a power of two.
*/
public static boolean isPowerOfTwo(int x) {
return x > 0 & (x & (x - 1)) == 0;
}

/**
* Returns 1 if {@code x < y} as unsigned integers, and 0 otherwise. Assumes that x - y fits into
* a signed int. The implementation is branch-free, and benchmarks suggest it is measurably (if
* narrowly) faster than the straightforward ternary expression.
*/
private static int lessThanBranchFree(int x, int y) {
// The double negation is optimized away by normal Java, but is necessary for GWT
// to make sure bit twiddling works as expected.
return ~~(x - y) >>> (Integer.SIZE - 1);
}

@SuppressWarnings("SameParameterValue")
private static void checkPositive(String role, int x) {
if (x <= 0) {
throw new IllegalArgumentException(role + " (" + x + ") must be > 0");
}
}

}
21 changes: 17 additions & 4 deletions src/main/java/com/aventrix/jnanoid/jnanoid/NanoIdUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

package com.aventrix.jnanoid.jnanoid;

import java.math.RoundingMode;
import java.security.SecureRandom;
import java.util.Random;

Expand All @@ -41,7 +42,7 @@ public final class NanoIdUtils {
* Instead, the class should be used as <code>NanoIdUtils.randomNanoId();</code>.
*/
private NanoIdUtils() {
//Do Nothing
throw new IllegalStateException();
}

/**
Expand Down Expand Up @@ -105,14 +106,18 @@ public static String randomNanoId(final Random random, final char[] alphabet, fi
throw new IllegalArgumentException("size must be greater than zero.");
}

final int mask = (2 << (int) Math.floor(Math.log(alphabet.length - 1) / Math.log(2))) - 1;
if(alphabet.length == 1){
return repeat(alphabet[0], size);
}

final int mask = (2 << MathUtils.log2(alphabet.length - 1, RoundingMode.FLOOR)) - 1;
final int step = (int) Math.ceil(1.6 * mask * size / alphabet.length);

final StringBuilder idBuilder = new StringBuilder();
final StringBuilder idBuilder = new StringBuilder(size);
final byte[] bytes = new byte[step];

while (true) {

final byte[] bytes = new byte[step];
random.nextBytes(bytes);

for (int i = 0; i < step; i++) {
Expand All @@ -131,4 +136,12 @@ public static String randomNanoId(final Random random, final char[] alphabet, fi
}

}

private static String repeat(char c, int size){
StringBuilder builder = new StringBuilder(size);
for(int i=0; i< size;++i){
builder.append(c);
}
return builder.toString();
}
}
31 changes: 31 additions & 0 deletions src/test/java/com/aventrix/jnanoid/InstantTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.aventrix.jnanoid;

import com.aventrix.jnanoid.jnanoid.MathUtils;
import com.aventrix.jnanoid.jnanoid.NanoIdUtils;
import org.junit.Test;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

/**
* @author fishzhao
* @since 2022-05-06
*/
public class InstantTest {

private static <T> T instant(Class<? extends T> type) throws Exception {
final Constructor<? extends T> declaredConstructor = type.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
return declaredConstructor.newInstance();
}

@Test(expected = InvocationTargetException.class)
public void MathUtils_Instant_ExceptionThrown() throws Exception {
instant(MathUtils.class);
}

@Test(expected = InvocationTargetException.class)
public void NanoIdUtils_Instant_ExceptionThrown() throws Exception {
instant(NanoIdUtils.class);
}
}
61 changes: 61 additions & 0 deletions src/test/java/com/aventrix/jnanoid/MathUtilsTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.aventrix.jnanoid;

import com.aventrix.jnanoid.jnanoid.MathUtils;
import org.junit.Test;

import java.math.BigDecimal;
import java.math.RoundingMode;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

/**
* @author fishzhao
* @since 2022-05-06
*/
public class MathUtilsTest {

private static int slowLog2(int x, RoundingMode roundingMode) {
double value = Math.log(x) / Math.log(2);
return BigDecimal.valueOf(value).setScale(0, roundingMode).intValue();
}

@Test
public void MathUtils_isPowerOf2_Verified() {
assertTrue(MathUtils.isPowerOfTwo(1));
assertTrue(MathUtils.isPowerOfTwo(2));
assertTrue(MathUtils.isPowerOfTwo(4));
assertTrue(MathUtils.isPowerOfTwo(256));
assertTrue(MathUtils.isPowerOfTwo(1 << 15));

assertFalse(MathUtils.isPowerOfTwo(3));
assertFalse(MathUtils.isPowerOfTwo(5));
assertFalse(MathUtils.isPowerOfTwo(456721));
assertFalse(MathUtils.isPowerOfTwo(-99));
}

@Test
public void MathUtils_log2_Verified() {
final RoundingMode[] roundingModes = RoundingMode.values();
for (int i = 2; i < 256; ++i) {
for (RoundingMode roundingMode : roundingModes) {
if (roundingMode != RoundingMode.UNNECESSARY)
assertEquals(slowLog2(i, roundingMode), MathUtils.log2(i, roundingMode));
}
}
for (int i = 0; i < Integer.SIZE - 1; ++i) {
assertEquals(i, MathUtils.log2(1 << i, RoundingMode.UNNECESSARY));
}
}

@Test(expected = IllegalArgumentException.class)
public void log2_negativeNumber_ExceptionThrown() {
MathUtils.log2(-99, RoundingMode.FLOOR);
}

@Test(expected = ArithmeticException.class)
public void log2_roundingMode_ExceptionThrown() {
MathUtils.log2(1821, RoundingMode.UNNECESSARY);
}
}