Java比C快2+倍(作为C++的子集)

Java is 2+ times faster than C (as a subset of C++)

本文关键字:C++ 作为 子集 Java      更新时间:2023-10-16

下面的代码是一种非常低效的乘法算法。它是作为测试目的编写的。我相信我写了相同的代码,但用不同的语言。

下面是运行代码的结果。

OS: Windows 7
language: C (as a subset of C++)
compiler: Visual C++
optimization option: /Ox /Oi /Ot /Oy /GL
running time (seconds): 40 +/- 1
compiler: MinGW/gcc
optimization option: -O3 march=native
running time (seconds): 81 +/- 1
compiler: MinGW/g++
optimization option: -O3 march=native
running time (seconds): 82 +/- 1
language: Java
compiler: Oracle JDK
VM: Oracle JVM
running time (seconds): 18 +/- 1

我相信我在 C 代码中做了一些可怕的事情,完全优化的编译器无法以任何方式优化出来。如果有任何大问题,请告诉我。我正在计划一个项目,该项目的一部分涉及大量计算。我决定用 C 语言编写这个核心计算部分,但有了这样的结果,我可能宁愿用 Java 编写所有内容;它更容易,甚至更快?我仍然相信C,所以如果我的代码中有任何问题,请告诉我。我所期望的是Java版本应该慢1.5倍或更多,但它在某种程度上优于C。

测试.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
typedef signed char byte;
typedef struct _Array
{
    byte *data;
    int len;
}
Array;
void new_Array(Array *a, int len)
{
    a->data = (byte *)malloc(len * sizeof(byte));
    a->len = len;
}
void del_Array(Array *a)
{
    free(a->data);
}
typedef struct _BUI
{
    Array num;
    int len;
}
BUI[1];
void new_BUI(BUI b, const char *s)
{
    int len = strlen(s);
    b->len = len;
    new_Array(&b->num, len);
    for (int i = 0; i < len; ++i)
    {
        b->num.data[i] = s[len - i - 1] - '0';
    }
}
void del_BUI(BUI b)
{
    del_Array(&b->num);
}
int BUI_cmp(const BUI a, const BUI b)
{
    if (a->len > b->len)
    {
        return 1;
    }
    if (a->len < b->len)
    {
        return -1;
    }
    for (int i = a->len - 1; i >= 0; --i)
    {
        if (a->num.data[i] > b->num.data[i])
        {
            return 1;
        }
        if (a->num.data[i] < b->num.data[i])
        {
            return -1;
        }
    }
    return 0;
}
#define MAX(A, B) (A > B ? A : B)
void BUI_add(BUI r, const BUI a, const BUI b)
{
    Array c;
    new_Array(&c, MAX(a->len, b->len) + 1);
    memset(c.data, 0, c.len);
    memcpy(c.data, a->num.data, a->len);
    for (int i = 0; i < b->len; ++i)
    {
        c.data[i] += b->num.data[i];
    }
    for (int i = 0; i < c.len - 1; ++i)
    {
        if (c.data[i] >= 10)
        {
            c.data[i + 1] += c.data[i] / 10;
            c.data[i] %= 10;
        }
    }
    del_Array(&r->num);
    r->num = c;
    r->len = c.len;
    for (int i = r->num.len - 1; r->num.data[i--] == 0; --r->len);
}
void BUI_mul(BUI r, const BUI a, const BUI b)
{
    BUI c;
    new_BUI(c, "0");
    {
        BUI one;
        new_BUI(one, "1");
        BUI i;
        new_BUI(i, "0");
        for (; BUI_cmp(i, a) < 0; BUI_add(i, i, one))
        {
            BUI_add(c, c, b);
        }
        del_BUI(one);
        del_BUI(i);
    }
    del_Array(&r->num);
    r->num = c->num;
    r->len = c->len;
}
void BUI_print(BUI b)
{
    for (int i = b->len - 1; i >= 0; --i)
    {
        putchar(b->num.data[i] + '0');
    }
}
int main(void)
{
    BUI a;
    new_BUI(a, "123456789");
    BUI b;
    new_BUI(b, "987654321");
    BUI_print(a);
    fputs(" x ", stdout);
    BUI_print(b);
    fputs(" = ", stdout);
    time_t start_time = clock();
    BUI_mul(a, a, b);
    time_t end_time = clock();
    BUI_print(a);
    del_BUI(a);
    del_BUI(b);
    printf("nelapsed time: %.3fn", (double)(end_time - start_time) / CLOCKS_PER_SEC);
    printf("%d %dn", a->num.len, a->len);
    return 0;
}

测试.java

import java.util.*;
class BUI
{
    byte[] num;
    int len;
    BUI(String s)
    {
        len = s.length();
        num = new byte[len];
        for (int i = 0; i < len; ++i)
        {
            num[i] = (byte)Character.getNumericValue(s.charAt(len - i - 1));
        }
    }
    int cmp(BUI b)
    {
        if (len > b.len)
        {
            return 1;
        }
        if (len < b.len)
        {
            return -1;
        }
        for (int i = len - 1; i >= 0; --i)
        {
            if (num[i] > b.num[i])
            {
                return 1;
            }
            if (num[i] < b.num[i])
            {
                return -1;
            }
        }
        return 0;
    }
    void add(BUI a, BUI b)
    {
        byte[] c = new byte[Math.max(a.len, b.len) + 1];
        Arrays.fill(c, (byte)0);
        System.arraycopy(a.num, 0, c, 0, a.num.length);
        for (int i = 0; i < b.len; ++i)
        {
            c[i] += b.num[i];
        }
        for (int i = 0; i < c.length - 1; ++i)
        {
            if (c[i] >= 10)
            {
                c[i + 1] += c[i] / 10;
                c[i] %= 10;
            }
        }
        num = c;
        len = c.length;
        for (int i = num.length - 1; num[i--] == 0; --len);
    }
    void mul(BUI a, BUI b)
    {
        BUI c = new BUI("0");
        {
            BUI one = new BUI("1");
            BUI i = new BUI("0");
            for (; i.cmp(a) < 0; i.add(i, one))
            {
                c.add(c, b);
            }
        }
        num = c.num;
        len = c.len;
    }
    void print()
    {
        for (int i = len - 1; i >= 0; --i)
        {
            System.out.print(num[i]);
        }
    }
}

public class Test
{
    public static void main(String[] args)
    {
        BUI a = new BUI("123456789");
        BUI b = new BUI("987654321");
        a.print();
        System.out.print(" x ");
        b.print();
        System.out.print(" = ");
        long start_time = System.currentTimeMillis();
        a.mul(a, b);
        long end_time = System.currentTimeMillis();
        a.print();
        System.out.printf("nelapsed time: %.3fn", (end_time - start_time) / 1000.0);
    }
}

"语言:C(作为C++的子集)"。

只是没有

C 不是 C++ 的子集。它们具有通用语法(大多数 C 语言),但大多数运行时检查都不同(取决于编译器),代码的解释方式不同(在少数情况下),并且大多数为 C 编写的代码都非常糟糕C++。

C++有很多工具可以加快您的算法实现速度(不确定有多少,但如果您测量正确,您肯定会看到更改)。

对于一个简单的示例,对字符数组使用 std::string。

我正在计划一个项目,该项目的一部分涉及大量计算。我决定用 C 语言编写这个核心计算部分,但有了这样的结果,我可能宁愿用 Java 编写所有内容;

去吧(也就是说,如果它对你来说更简单,就用 Java 编写它)。你可以在 C 中获得更好的性能,就像你可以在 Java 中获得更好的性能一样。由您决定在优化算法和代码方面投入了多少时间和精力。

计算速度不会来自使用优化选项运行编译器 - 嗯,它确实如此,但与算法优化的速度相比,这是相对较小的。不过,您可以通过优化您熟悉的开发工具链,然后磕磕绊绊地使用您不熟悉的语言来获得更快的速度。

它更容易,甚至更快?我仍然相信C,所以如果我的代码中有任何问题,请告诉我。我所期望的是Java版本应该慢1.5倍或更多,但它在某种程度上优于C。

你的逻辑是有缺陷的。

您编写的比较不能以任何方式代表 C 和 Java 之间的速度差异(C++ 和 Java 之间的速度差异更小)。它代表比较这两个实现,用不同的语言编译,而不是语言本身。

换句话说,像这样比较两个应用程序,即使它们看起来是等效的,也不会比较语言(或编译器)的速度。它只是比较两个不同的程序,运行同一算法的不同版本。这是一个特例。

您的 C 代码已编译。它将在不同的硬件上具有不同的性能特征(例如,两个处理器与四个处理器)。

您的 Java 代码是字节编译的。在大多数 Java VM 运行之前,它将对其进行优化,以最好地匹配代码将在其上运行的平台。

最后,你可能会在 C 或 C++ 中比在 Java

中更积极地进行优化,如果你需要编写真正对性能至关重要的代码,那么你可能会获得 Java 无法比拟的 C 或C++代码,因为它会比运行 Java VM 本身所需的速度阈值更快。

但是,在特定情况下,这种优化需要花费大量的时间和精力来分析和优化,并且大多数应用程序域不需要它。如果您不知道是否需要这种级别的性能,您可能不需要。

您的 C 版本有很多不必要的内存分配,相对昂贵。如果使用就地加法和递增函数(见下文),则可以大幅提高性能:

  • 原始 C 代码 = 200 秒
  • 使用就地加法 = 7.6 秒
  • 使用就地加法和增量 = 6.5 秒

速度的差异完全来自分配数量的减少,从原始代码中的 2.5 亿 (!) 减少到修改后代码中的 30 个。因此,您的原始代码实际上只是测量每种语言中内存管理器的效率,而不是实际的 bignum 乘法算法。

您可以在 Java 代码中使用相同的优化来获得类似的速度改进。不过要小心不要过多地玩基准测试/优化游戏。在特定版本上有足够的工作,您可能会比另一个版本更快地获得它。您通常不应该仅根据"X 快 1%"来决定您的语言。

要使用新的就地函数,只需将乘法循环更改为:

    for (; BUI_cmp(i, a) < 0; BUI_inc(i))
    {
        BUI_addinplace(c, b);
    }

使用的就地添加/递增函数如下:

void BUI_addinplace(BUI a, const BUI b) //a += b
{
    int maxSize = MAX(a->len, b->len) + 1;
    if (a->num.len < maxSize)
    {
        Array tmp;
        new_Array(&tmp, maxSize);
        memset(tmp.data, 0, tmp.len);
        memcpy(tmp.data, a->num.data, a->len);
        del_Array(&a->num);
        a->num = tmp;
    }
    for (int i = 0; i < b->len; ++i)
    {
        a->num.data[i] += b->num.data[i];
    }
    int maxLen = a->len;
    for (int i = 0; i < a->len; ++i)
    {
        if (a->num.data[i] >= 10)
        {
            a->num.data[i + 1] += a->num.data[i] / 10;
            a->num.data[i] %= 10;
            maxLen = i + 2;
        }
    }
    if (maxLen > a->len) a->len = maxLen;
}

void BUI_inc(BUI a) //a += 1
{
    int maxSize = a->len + 1;
    if (a->num.len < maxSize)
    {
        ++numAllocations;
        Array tmp;
        new_Array(&tmp, maxSize);
        memset(tmp.data, 0, tmp.len);
        memcpy(tmp.data, a->num.data, a->len);
        del_Array(&a->num);
        a->num = tmp;
    }
    ++a->num.data[0];
    if (a->num.data[0] < 10) return;
    int maxLen = a->len;
    for (int i = 0; i < a->len; ++i)
    {
        if (a->num.data[i] >= 10)
        {
            a->num.data[i + 1] += a->num.data[i] / 10;
            a->num.data[i] %= 10;
            maxLen = i + 2;
        }
        else {
            break;
        }
    }
    if (maxLen > a->len) a->len = maxLen;
}

正如评论和其他答案中容易提到的,瓶颈是对malloc的近2.5亿次调用。

我通常不写 C,所以请原谅我的非惯用代码,但这里有一个非常原始的分配器(它有很多限制,可能仍然存在错误和简单的优化机会,可以使用一些断言)在这种情况下,它的性能比 malloc 高出很多。

#define BUFFER_SIZE 1048576 //reserve 1MB, although we only use 417bytes
typedef struct mem_block_hdr
{
    unsigned short size; //size of the memory block, excluding header
    char free;           //is this block free
    char cont;           //not used
}mem_block_hdr_t;
struct _myallocdata
{
    char mem[BUFFER_SIZE];
    unsigned int highest_header_pos;
} my_alloc_data;
void init_block_hdr(void *mem)
{
    mem_block_hdr_t *head = (mem_block_hdr_t*) mem;
    head->size = USHRT_MAX;
    head->free = 1;
    head->cont = 0;
}
void init_my_alloc()
{
    init_block_hdr(my_alloc_data.mem);
    my_alloc_data.highest_header_pos = 0;
}
mem_block_hdr_t *next_header(mem_block_hdr_t *curr)
{
    return (mem_block_hdr_t*) ((char*) curr + sizeof(mem_block_hdr_t) + curr->size);
}
mem_block_hdr_t *find_next_free(unsigned int size)
{
    void * ret;
    char end_reached = 0;
    mem_block_hdr_t *head = (mem_block_hdr_t*) my_alloc_data.mem;
    while(
        (!head->free || head->size < size) &&
        ((char*) head - my_alloc_data.mem ) < my_alloc_data.highest_header_pos
        )
    {
        head = next_header(head);
    }
    return head;
}
void *my_alloc(unsigned int size)
{
    mem_block_hdr_t *header = find_next_free(size);
    unsigned int diff = (char*) header - my_alloc_data.mem ;
    if (header->size == USHRT_MAX)
    {
        header->size = size;
    }
    header->free = 0;
    if (diff >= my_alloc_data.highest_header_pos)
    {
        mem_block_hdr_t *new_high = next_header(header);
        init_block_hdr(new_high);
        my_alloc_data.highest_header_pos = ((char*) new_high) - my_alloc_data.mem;
    }
    return (void *)++header;
}
void my_free(void *mem)
{
    mem_block_hdr_t *hdr =(mem_block_hdr_t *) ((char *)mem - sizeof(mem_block_hdr_t));
    hdr->free = 1;
}
void new_Array(Array *a, int len)
{
    //a->data = (byte *) malloc(len * sizeof(byte));
    a->data = (byte *) my_alloc(len * sizeof(byte));
    a->len = len;
}
void del_Array(Array *a)
{
    //free(a->data);
    my_free(a->data);
}
//calling init_my_alloc() in main before using it

我得到的数字是:

123456789 x 987654321 = 121932631112635269
elapsed time (custom alloc): 25.546
19 18
123456789 x 987654321 = 121932631112635269
elapsed time (malloc): 290.118
19 18

编辑:似乎我选择了对malloc调用不太友好的MSVC标志,使用gcc我得到这个:

123456789 x 987654321 = 121932631112635269
elapsed time (custom alloc): 30.703
19 18
123456789 x 987654321 = 121932631112635269
elapsed time (malloc): 46.406
19 18

我敢打赌那里有很多分配器实现(因为有时似乎每个更大的 C 项目都有自己的几个)。

无论如何,如果你觉得编写Java更舒服,并且你认为它足以满足你的目的,那么你没有理由不使用它。编程语言毕竟只是工具。