Kiểm tra phân chia nhanh hơn toán tử%?


23

Tôi nhận thấy một điều tò mò trên máy tính của tôi. * Bài kiểm tra chia nhỏ viết tay nhanh hơn đáng kể so với người %vận hành. Hãy xem xét ví dụ tối thiểu:

* Máy xâu chuỗi AMD Ryzen 2990WX, GCC 9.2.0

static int divisible_ui_p(unsigned int m, unsigned int a)
{
    if (m <= a) {
        if (m == a) {
            return 1;
        }

        return 0;
    }

    m += a;

    m >>= __builtin_ctz(m);

    return divisible_ui_p(m, a);
}

Ví dụ được giới hạn bởi lẻ am > 0. Tuy nhiên, nó có thể dễ dàng khái quát cho tất cả am. Mã chỉ chuyển đổi phân chia thành một loạt các bổ sung.

Bây giờ hãy xem xét chương trình thử nghiệm được biên dịch với -std=c99 -march=native -O3:

    for (unsigned int a = 1; a < 100000; a += 2) {
        for (unsigned int m = 1; m < 100000; m += 1) {
#if 1
            volatile int r = divisible_ui_p(m, a);
#else
            volatile int r = (m % a == 0);
#endif
        }
    }

... và kết quả trên máy tính của tôi:

| implementation     | time [secs] |
|--------------------|-------------|
| divisible_ui_p     |    8.52user |
| builtin % operator |   17.61user |

Do đó nhanh hơn 2 lần.

Câu hỏi: Bạn có thể cho tôi biết mã hoạt động trên máy của bạn như thế nào không? Có bỏ lỡ cơ hội tối ưu hóa trong GCC không? Bạn có thể làm bài kiểm tra này thậm chí nhanh hơn?


CẬP NHẬT: Theo yêu cầu, đây là một ví dụ tái sản xuất tối thiểu:

#include <assert.h>

static int divisible_ui_p(unsigned int m, unsigned int a)
{
    if (m <= a) {
        if (m == a) {
            return 1;
        }

        return 0;
    }

    m += a;

    m >>= __builtin_ctz(m);

    return divisible_ui_p(m, a);
}

int main()
{
    for (unsigned int a = 1; a < 100000; a += 2) {
        for (unsigned int m = 1; m < 100000; m += 1) {
            assert(divisible_ui_p(m, a) == (m % a == 0));
#if 1
            volatile int r = divisible_ui_p(m, a);
#else
            volatile int r = (m % a == 0);
#endif
        }
    }

    return 0;
}

được biên dịch với gcc -std=c99 -march=native -O3 -DNDEBUGtrên AMD Ryzen Threadripper 2990WX với

gcc --version
gcc (Gentoo 9.2.0-r2 p3) 9.2.0

CẬP NHẬT2: Theo yêu cầu, phiên bản có thể xử lý bất kỳ am(nếu bạn cũng muốn tránh tràn số nguyên, thử nghiệm phải được thực hiện với loại số nguyên dài gấp đôi số nguyên đầu vào):

int divisible_ui_p(unsigned int m, unsigned int a)
{
#if 1
    /* handles even a */
    int alpha = __builtin_ctz(a);

    if (alpha) {
        if (__builtin_ctz(m) < alpha) {
            return 0;
        }

        a >>= alpha;
    }
#endif

    while (m > a) {
        m += a;
        m >>= __builtin_ctz(m);
    }

    if (m == a) {
        return 1;
    }

#if 1
    /* ensures that 0 is divisible by anything */
    if (m == 0) {
        return 1;
    }
#endif

    return 0;
}

Bình luận không dành cho thảo luận mở rộng; cuộc trò chuyện này đã được chuyển sang trò chuyện .
Samuel Liew

Tôi cũng muốn xem một bài kiểm tra mà bạn thực sự khẳng định rằng hai rs mà bạn tính toán thực sự bằng nhau.
Mike Nakis

@MikeNakis Tôi vừa thêm điều đó.
DaBler

2
Hầu hết sử dụng thực tế a % bbnhỏ hơn nhiều a. Thông qua hầu hết các lần lặp trong trường hợp thử nghiệm của bạn, chúng có kích thước tương tự hoặc blớn hơn và phiên bản của bạn có thể nhanh hơn trên nhiều CPU trong các tình huống đó.
Matt Timmermans

Câu trả lời:


11

Những gì bạn đang làm được gọi là giảm sức mạnh: thay thế một hoạt động đắt tiền bằng một loạt các hoạt động giá rẻ.

Hướng dẫn mod trên nhiều CPU rất chậm, vì trước đây nó không được thử nghiệm trong một số điểm chuẩn chung và do đó các nhà thiết kế đã tối ưu hóa các hướng dẫn khác thay thế. Thuật toán này sẽ hoạt động kém hơn nếu nó phải thực hiện nhiều lần lặp và %sẽ hoạt động tốt hơn trên CPU khi nó chỉ cần hai chu kỳ xung nhịp.

Cuối cùng, hãy lưu ý rằng có nhiều phím tắt để lấy phần còn lại của phép chia theo các hằng số cụ thể. (Mặc dù trình biên dịch thường sẽ chăm sóc điều này cho bạn.)


trong lịch sử không được kiểm tra trong một số điểm chuẩn chung - Cũng bởi vì sự phân chia vốn đã lặp đi lặp lại và khó thực hiện nhanh chóng! x86 ít nhất còn lại là một phần của div/ idivđã nhận được một số tình yêu trong Intel Penryn, Broadwell và IceLake (bộ chia phần cứng cơ số cao hơn)
Peter Cordes

1
Hiểu biết của tôi về "giảm sức mạnh" là bạn thay thế một thao tác nặng trong một vòng lặp bằng một thao tác nhẹ hơn, ví dụ thay vì x = i * constmỗi lần lặp bạn thực hiện x += constmỗi lần lặp. Tôi không nghĩ việc thay thế một bội số bằng một vòng lặp shift / add sẽ được gọi là giảm sức mạnh. vi.wikipedia.org/wiki/ , nói thuật ngữ này có thể được sử dụng theo cách này, nhưng với một lưu ý "Tài liệu này đang bị tranh chấp. Nó được mô tả tốt hơn là tối ưu hóa lổ nhìn trộm và gán lệnh."
Peter Cordes

9

Tôi sẽ tự trả lời câu hỏi của mình. Dường như tôi trở thành nạn nhân của dự đoán chi nhánh. Kích thước lẫn nhau của các toán hạng dường như không quan trọng, chỉ có thứ tự của chúng.

Hãy xem xét việc thực hiện sau đây

int divisible_ui_p(unsigned int m, unsigned int a)
{
    while (m > a) {
        m += a;
        m >>= __builtin_ctz(m);
    }

    if (m == a) {
        return 1;
    }

    return 0;
}

và các mảng

unsigned int A[100000/2];
unsigned int M[100000-1];

for (unsigned int a = 1; a < 100000; a += 2) {
    A[a/2] = a;
}
for (unsigned int m = 1; m < 100000; m += 1) {
    M[m-1] = m;
}

được / không được xáo trộn bằng cách sử dụng chức năng xáo trộn .

Không xáo trộn, kết quả vẫn là

| implementation     | time [secs] |
|--------------------|-------------|
| divisible_ui_p     |    8.56user |
| builtin % operator |   17.59user |

Tuy nhiên, một khi tôi xáo trộn các mảng này, kết quả sẽ khác

| implementation     | time [secs] |
|--------------------|-------------|
| divisible_ui_p     |   31.34user |
| builtin % operator |   17.53user |
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.