Làm thế nào để bạn tính toán cơ sở log 2 trong Java cho các số nguyên?


138

Tôi sử dụng hàm sau để tính log cơ sở 2 cho số nguyên:

public static int log2(int n){
    if(n <= 0) throw new IllegalArgumentException();
    return 31 - Integer.numberOfLeadingZeros(n);
}

Liệu nó có hiệu suất tối ưu?

Có ai biết chức năng API J2SE sẵn sàng cho mục đích đó không?

CẬP NHẬT1 Đáng ngạc nhiên đối với tôi, mỹ phẩm điểm nổi dường như nhanh hơn so với mỹ phẩm số nguyên.

CẬP NHẬT2 Do ý kiến ​​tôi sẽ tiến hành điều tra chi tiết hơn.

CẬP NHẬT Hàm số học số nguyên của tôi nhanh hơn 10 lần so với Math.log (n) /Math.log (2).


1
Làm thế nào bạn kiểm tra hiệu suất của điều này? Trên Hệ thống của tôi (Core i7, jdk 1.6 x64), phiên bản số nguyên nhanh hơn gần 10 lần so với phiên bản dấu phẩy động. Hãy chắc chắn thực sự làm một cái gì đó với kết quả của hàm để JIT không thể loại bỏ hoàn toàn phép tính!
x4u

Bạn nói đúng. Tôi không sử dụng kết quả tính toán và trình biên dịch đã tối ưu hóa một cái gì đó. Bây giờ tôi có kết quả giống như bạn - hàm số nguyên nhanh hơn 10 lần (Core 2 Duo, jdk 1.6
c64

6
Điều này mang lại hiệu quả cho bạn Math.floor(Math.log(n)/Math.log(2)), vì vậy nó không thực sự tính toán cơ sở 2!
Dori

Câu trả lời:


74

Nếu bạn đang suy nghĩ về việc sử dụng dấu phẩy động để trợ giúp với số nguyên học, bạn phải cẩn thận.

Tôi thường cố gắng tránh tính toán FP bất cứ khi nào có thể.

Hoạt động dấu phẩy động không chính xác. Bạn không bao giờ có thể biết chắc chắn những gì sẽ (int)(Math.log(65536)/Math.log(2))đánh giá. Ví dụ: Math.ceil(Math.log(1<<29) / Math.log(2))là 30 trên PC của tôi về mặt toán học, nó phải chính xác là 29. Tôi không tìm thấy giá trị cho x khi (int)(Math.log(x)/Math.log(2))thất bại (chỉ vì chỉ có 32 giá trị "nguy hiểm"), nhưng điều đó không có nghĩa là nó sẽ hoạt động cùng một cách trên bất kỳ PC nào.

Thủ thuật thông thường ở đây là sử dụng "epsilon" khi làm tròn. Giống như (int)(Math.log(x)/Math.log(2)+1e-10)không bao giờ nên thất bại. Sự lựa chọn của "epsilon" này không phải là một nhiệm vụ tầm thường.

Trình diễn nhiều hơn, sử dụng một nhiệm vụ tổng quát hơn - cố gắng thực hiện int log(int x, int base):

Mã kiểm tra:

static int pow(int base, int power) {
    int result = 1;
    for (int i = 0; i < power; i++)
        result *= base;
    return result;
}

private static void test(int base, int pow) {
    int x = pow(base, pow);
    if (pow != log(x, base))
        System.out.println(String.format("error at %d^%d", base, pow));
    if(pow!=0 && (pow-1) != log(x-1, base))
        System.out.println(String.format("error at %d^%d-1", base, pow));
}

public static void main(String[] args) {
    for (int base = 2; base < 500; base++) {
        int maxPow = (int) (Math.log(Integer.MAX_VALUE) / Math.log(base));
        for (int pow = 0; pow <= maxPow; pow++) {
            test(base, pow);
        }
    }
}

Nếu chúng ta sử dụng logarit đơn giản nhất,

static int log(int x, int base)
{
    return (int) (Math.log(x) / Math.log(base));
}

bản in này:

error at 3^5
error at 3^10
error at 3^13
error at 3^15
error at 3^17
error at 9^5
error at 10^3
error at 10^6
error at 10^9
error at 11^7
error at 12^7
...

Để hoàn toàn thoát khỏi lỗi, tôi đã phải thêm epsilon trong khoảng từ 1e-11 đến 1e-14. Bạn có thể nói điều này trước khi thử nghiệm? Tôi chắc chắn không thể.


3
"Điều đó không có nghĩa là nó sẽ hoạt động theo cùng một cách trên bất kỳ PC nào" - Nó sẽ như thế nào nếu bạn sử dụng strictfp, phải không?
Ken

@Ken: Có thể ... Nhưng bạn chỉ có thể chắc chắn sau khi liệt kê đầy đủ tất cả các giá trị đầu vào có thể. (chúng tôi may mắn có rất ít trong số họ ở đây)
Rotsor

2
Về mặt kỹ thuật, có, nhưng điều đó đúng với bất kỳ chức năng nào. Tại một số điểm, bạn phải tin tưởng rằng nếu bạn sử dụng tài liệu có sẵn và kiểm tra một số phần nhỏ được lựa chọn tốt nhưng nhỏ gọn của "tất cả các giá trị đầu vào có thể", thì chương trình của bạn sẽ hoạt động đủ tốt. strictfpDường như đã thực sự nhận được rất nhiều điều tào lao vì thực tế, nghiêm ngặt. :-)
Ken

làm thế nào về return ((long)Math.log(x) / (long)Math.log(base));để giải quyết tất cả các lỗi?
Không phải là một lỗi

92

Đây là hàm mà tôi sử dụng cho tính toán này:

public static int binlog( int bits ) // returns 0 for bits=0
{
    int log = 0;
    if( ( bits & 0xffff0000 ) != 0 ) { bits >>>= 16; log = 16; }
    if( bits >= 256 ) { bits >>>= 8; log += 8; }
    if( bits >= 16  ) { bits >>>= 4; log += 4; }
    if( bits >= 4   ) { bits >>>= 2; log += 2; }
    return log + ( bits >>> 1 );
}

Nó nhanh hơn một chút so với Integer.numberOfLeadZeros () (20-30%) và nhanh hơn gần 10 lần (jdk 1.6 x64) so ​​với triển khai dựa trên Math.log () như thế này:

private static final double log2div = 1.000000000001 / Math.log( 2 );
public static int log2fp0( int bits )
{
    if( bits == 0 )
        return 0; // or throw exception
    return (int) ( Math.log( bits & 0xffffffffL ) * log2div );
}

Cả hai hàm trả về cùng một kết quả cho tất cả các giá trị đầu vào có thể.

Cập nhật: JIT máy chủ Java 1.7 có thể thay thế một vài hàm toán học tĩnh bằng các triển khai thay thế dựa trên nội tại của CPU. Một trong những chức năng đó là Integer.numberOfLeadZeros (). Vì vậy, với máy chủ VM 1.7 hoặc mới hơn, việc triển khai như câu hỏi trong câu hỏi thực sự nhanh hơn một chút so với binlogở trên. Thật không may, khách hàng JIT dường như không có tối ưu hóa này.

public static int log2nlz( int bits )
{
    if( bits == 0 )
        return 0; // or throw exception
    return 31 - Integer.numberOfLeadingZeros( bits );
}

Việc triển khai này cũng trả về kết quả tương tự cho tất cả 2 ^ 32 giá trị đầu vào có thể như hai triển khai khác mà tôi đã đăng ở trên.

Dưới đây là thời gian chạy thực tế trên PC của tôi (Sandy Bridge i7):

Máy khách VM JDK 1.7 32 Bits:

binlog:         11.5s
log2nlz:        16.5s
log2fp:        118.1s
log(x)/log(2): 165.0s

Máy chủ JDK 1.7 x64 VM:

binlog:          5.8s
log2nlz:         5.1s
log2fp:         89.5s
log(x)/log(2): 108.1s

Đây là mã kiểm tra:

int sum = 0, x = 0;
long time = System.nanoTime();
do sum += log2nlz( x ); while( ++x != 0 );
time = System.nanoTime() - time;
System.out.println( "time=" + time / 1000000L / 1000.0 + "s -> " + sum );

9
Lệnh của x86 BSR32 - numberOfLeadingZeros, nhưng không được xác định bằng 0, do đó, trình biên dịch (JIT) phải kiểm tra khác không nếu không thể chứng minh rằng nó không phải. Các phần mở rộng tập lệnh BMI (Haswell và mới hơn) được giới thiệu LZCNT, thực hiện đầy đủ numberOfLeadingZeroschính xác, trong một hướng dẫn duy nhất. Cả hai đều có độ trễ 3 chu kỳ, 1 thông lượng cho mỗi chu kỳ. Vì vậy, tôi hoàn toàn khuyên bạn nên sử dụng numberOfLeadingZeros, vì điều đó giúp dễ dàng cho một JVM tốt. (Một điều kỳ lạ lzcntlà nó có sự phụ thuộc sai vào giá trị cũ của thanh ghi mà nó ghi đè lên.)
Peter Cordes

Tôi quan tâm nhất đến nhận xét của bạn về việc thay thế nội tại CPU JIT của máy chủ Java 1.7. Bạn có URL tham khảo không? (Liên kết mã nguồn JIT cũng OK.)
kevinarpe

37

Thử Math.log(x) / Math.log(2)


8
Về mặt toán học, điều này là chính xác, xin lưu ý rằng có nguy cơ tính toán sai do số học dấu phẩy động không chính xác, như được giải thích trong câu trả lời của Rotsor.
leeyuiwah

28

bạn có thể sử dụng danh tính

            log[a]x
 log[b]x = ---------
            log[a]b

vì vậy điều này sẽ được áp dụng cho log2.

            log[10]x
 log[2]x = ----------
            log[10]2

chỉ cần cắm cái này vào phương thức java Math log10 ....

http://mathforum.org/l Library / drmath / view /55565.html


3
Về mặt toán học, điều này là chính xác, xin lưu ý rằng có nguy cơ tính toán sai do số học dấu phẩy động không chính xác, như được giải thích trong câu trả lời của Rotsor.
leeyuiwah

18

Tại sao không:

public static double log2(int n)
{
    return (Math.log(n) / Math.log(2));
}

6
Về mặt toán học, điều này là chính xác, xin lưu ý rằng có nguy cơ tính toán sai do số học dấu phẩy động không chính xác, như được giải thích trong câu trả lời của Rotsor.
leeyuiwah

9

Có chức năng trong thư viện ổi:

LongMath.log2()

Vì vậy, tôi đề nghị sử dụng nó.


Làm thế nào tôi có thể thêm gói này vào ứng dụng của tôi?
Elvin Mammadov

Tải về jar từ đây và thêm nó vào đường dẫn xây dựng dự án của bạn.
Debosmit Ray

2
Tôi có nên thêm một thư viện vào ứng dụng của mình chỉ để sử dụng một chức năng?
Tash Pemhiwa

7
Tại sao chính xác bạn sẽ đề nghị sử dụng nó? Đọc nhanh nguồn Guava cho thấy rằng nó thực hiện tương tự như phương pháp của OP (một vài dòng mã được hiểu rất rõ), với chi phí thêm một phụ thuộc vô dụng. Chỉ vì Google cung cấp một cái gì đó không làm cho nó tốt hơn là tự mình hiểu vấn đề và giải pháp.
Dave

3

Để thêm vào câu trả lời x4u, cung cấp cho bạn sàn của nhật ký nhị phân của một số, hàm này trả về trần của nhật ký nhị phân của một số:

public static int ceilbinlog(int number) // returns 0 for bits=0
{
    int log = 0;
    int bits = number;
    if ((bits & 0xffff0000) != 0) {
        bits >>>= 16;
        log = 16;
    }
    if (bits >= 256) {
        bits >>>= 8;
        log += 8;
    }
    if (bits >= 16) {
        bits >>>= 4;
        log += 4;
    }
    if (bits >= 4) {
        bits >>>= 2;
        log += 2;
    }
    if (1 << log < number)
        log++;
    return log + (bits >>> 1);
}

Biến "số" ở đâu?
barteks2x

3

Một số trường hợp chỉ hoạt động khi tôi sử dụng Math.log10:

public static double log2(int n)
{
    return (Math.log10(n) / Math.log10(2));
}

0

hãy thêm:

int[] fastLogs;

private void populateFastLogs(int length) {
    fastLogs = new int[length + 1];
    int counter = 0;
    int log = 0;
    int num = 1;
    fastLogs[0] = 0;
    for (int i = 1; i < fastLogs.length; i++) {
        counter++;
        fastLogs[i] = log;
        if (counter == num) {
            log++;
            num *= 2;
            counter = 0;
        }
    }
}

Nguồn: https://github.com/pochuan/cs166/blob/master/ps1/rmq/SparseTableRMQ.java


Điều đó sẽ tạo ra một bảng tra cứu. OP đã yêu cầu một cách nhanh hơn để "tính toán" logarit.
Dave

-4

Để tính toán cơ sở log 2 của n, có thể sử dụng biểu thức sau:

double res = log10(n)/log10(2);

2
Câu trả lời này đã được đăng nhiều lần và đã được nhận thấy là có khả năng không chính xác do lỗi làm tròn số. Lưu ý OP yêu cầu giá trị tích phân; không rõ ràng độ chính xác làm tròn cần được sử dụng để đi từ đây đến một số nguyên.
AnotherParker
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.