当前位置:首页 > 文章列表 > Golang > Go问答 > 为什么此代码在 Go 中与 Java 中运行需要更长的时间

为什么此代码在 Go 中与 Java 中运行需要更长的时间

来源:stackoverflow 2024-03-16 12:21:32 0浏览 收藏
推广推荐
免费电影APP ➜
支持 PC / 移动端,安全直达

Go 语言中的循环比 Java 语言中慢的原因尚未明确。一些人认为这是由于 Go 编译器生成的代码效率较低,而另一些人则认为 Java 的 JIT 优化器对循环进行了重大优化。为了探索这一差异,本文比较了 Go 和 Java 中一个简单循环的性能,该循环对 200 亿个数字进行计数。结果显示,Go 版本的循环比 Java 版本慢得多,平均运行时间为 5,851 毫秒,而 Java 版本的平均运行时间为 392 毫秒。本文分析了 Go 编译器生成的代码,发现它非常复杂且冗长,而 Java 字节码相对简单。本文还讨论了 JIT 优化可能对 Java 代码产生的影响,并指出这种优化可能预先计算循环变量的值,从而显著提高性能。

问题内容

最近开始学习 go,我是一个忠实的粉丝,有 java 背景。

我以不同的方式比较了这些语言,令我惊讶的是,与 java 相比,计数到 200 亿的简单循环在 golang 中花费的时间要长得多。

想知道是否有人可以对我在这里遗漏的内容提供任何见解。这就是我所做的:

java

编写以下代码,从普通的 main() 方法执行它,使用 gradle 构建可执行 jar,并使用以下命令从命令行执行它: java -jar build/libs/my-executable.jar

private void counttotwentybillion() {
    long count = 0;
    long start = system.currenttimemillis();
    for (int k = 0; k < 10; k++) {
        system.out.println("on step " + k);
        for (int i = 0; i < 2_000_000_000; i++) {
            // do nothing but count
            count++;
        }
    }
    long end = system.currenttimemillis();
    system.out.println("total time took: " + (end - start) + " ms to get at count: " + count);
}

通过 3 个单独的试验,我得到了以下结果:

// total time took: 396 ms to get at count: 20000000000
// total time took: 393 ms to get at count: 20000000000
// total time took: 388 ms to get at count: 20000000000
// 392 ms average

在 go 中构建此文件,使用“go build”构建并使用 在命令行中执行。/loop-counter

package main

import (
    "fmt"
    "time"
)

func main() {
    count := 0

    nanos := time.now().unixnano()
    start := nanos / 1000000

    for i := 0; i < 10; i++ {
        fmt.printf("on step %d\n", i)
        for k := 0; k < 2000000000; k++ {
            count++
        }
    }
    nanos = time.now().unixnano()
    end := nanos / 1000000

    timelength := end - start

    fmt.printf("total time took: %d ms to get at count: %d\n", timelength, count)

}

经过 3 次单独的试验,我得到了以下结果:

// Total time took: 5812 ms to get at count: 20000000000
// Total time took: 5834 ms to get at count: 20000000000
// Total time took: 5907 ms to get at count: 20000000000
// 5,851 ms average

我开始期待 go 会更快,但最终却感到惊讶。所有试验均在同一台机器上、相同条件下进行。

谁能告诉我什么?

谢谢


解决方案


我不是 go 专家,但 java 确实优化了循环。

假设您有一个带有 3ghz 的单核处理器,每条指令的时间为 0.3ns,我们假设每个增量都是一条指令。因此 0.3ns *200 亿 = 6s 是在没有任何优化的情况下粗略估计的性能。

您可以通过向您的程序提供 -xx:loopunrolllimit=1 来验证 java 是否在此处进行了一些欺骗。这告诉 jvm 几乎不进行循环展开,因此可以防止大多数 jit 优化在您的示例中发生。

这样做后,您的 java 示例的运行时现在在我的机器上为 6s,这与 go 基准测试相当。

go 版本中可能还有一个选项可以启用循环展开等优化(请参阅 go 手册)。

最后,这再次表明,微基准测试很难正确执行。他们经常自欺欺人地假设一些不正确的事情。

以下是我的一些观察结果。我将展示一些通过编译该程序获得的英特尔语法汇编代码。我使用的是Compiler Explorer。要理解下面的内容,您不必了解很多汇编,这里最重要的元素是大小,它越大,速度越慢。如果可以的话我会把这篇文章缩小,但是生成的代码出奇的庞大,而且我对 go 的了解还不够,不知道什么是无用的。如果您想查看汇编中每个语句转换成的内容,编译器资源管理器将为您突出显示所有内容。

tl;dr
在我看来,go 编译器是一个灾难性的混乱,c++ 代码得到了很好的优化,而 java 与 go 相比很小。 jit'ing 可能对 java 代码产生重大影响,对于分解内联优化的循环来说也可能太复杂(预先计算 count 的值)。

go 代码编译成这个怪物:

text    "".main(sb), $224-0
movq    (tls), cx
leaq    -96(sp), ax
cmpq    ax, 16(cx)
jls     835
subq    $224, sp
movq    bp, 216(sp)
leaq    216(sp), bp
funcdata        $0, gclocals·f6bd6b3389b872033d462029172c8612(sb)
funcdata        $1, gclocals·17283ea8379a997487dd6f8baf7ae6ea(sb)
pcdata  $0, $0
call    time.now(sb)
movq    16(sp), ax
movq    8(sp), cx
movq    (sp), dx
movq    dx, time.t·2+160(sp)
movq    cx, time.t·2+168(sp)
movq    ax, time.t·2+176(sp)
movq    time.t·2+160(sp), ax
movq    ax, cx
shrq    $63, ax
shlq    $63, ax
testq   $-1, ax
jeq     806
movq    cx, dx
shlq    $1, cx
shrq    $31, cx
movq    $59453308800, bx
addq    bx, cx
andq    $1073741823, dx
movlqsx dx, dx
imulq   $1000000000, cx
addq    dx, cx
movq    $-6795364578871345152, dx
addq    dx, cx
movq    $4835703278458516699, ax
imulq   cx
sarq    $63, cx
sarq    $18, dx
subq    cx, dx
movq    dx, "".start+72(sp)
xorl    ax, ax
movq    ax, cx
jmp     257
incq    cx
incq    ax
cmpq    cx, $2000000000
jlt     213
movq    "".i+80(sp), si
incq    si
movq    "".start+72(sp), dx
movq    $59453308800, bx
movq    ax, cx
movq    si, ax
movq    cx, "".count+88(sp)
cmpq    ax, $10
jge     404
movq    ax, "".i+80(sp)
movq    ax, ""..autotmp_24+112(sp)
xorps   x0, x0
movups  x0, ""..autotmp_23+120(sp)
leaq    type.int(sb), cx
movq    cx, (sp)
leaq    ""..autotmp_24+112(sp), dx
movq    dx, 8(sp)
pcdata  $0, $1
call    runtime.convt2e64(sb)
movq    24(sp), ax
movq    16(sp), cx
movq    cx, ""..autotmp_23+120(sp)
movq    ax, ""..autotmp_23+128(sp)
leaq    go.string."on step %d\n"(sb), ax
movq    ax, (sp)
movq    $11, 8(sp)
leaq    ""..autotmp_23+120(sp), cx
movq    cx, 16(sp)
movq    $1, 24(sp)
movq    $1, 32(sp)
pcdata  $0, $1
call    fmt.printf(sb)
movq    "".count+88(sp), ax
xorl    cx, cx
jmp     219
pcdata  $0, $2
call    time.now(sb)
movq    16(sp), ax
movq    8(sp), cx
movq    (sp), dx
movq    dx, time.t·2+136(sp)
movq    cx, time.t·2+144(sp)
movq    ax, time.t·2+152(sp)
movq    time.t·2+136(sp), ax
movq    ax, cx
shrq    $63, ax
shlq    $63, ax
testq   $-1, ax
jeq     787
movq    cx, dx
shlq    $1, cx
shrq    $31, cx
movq    $59453308800, bx
addq    bx, cx
imulq   $1000000000, cx
andq    $1073741823, dx
movlqsx dx, dx
addq    dx, cx
movq    $-6795364578871345152, dx
leaq    (dx)(cx*1), ax
movq    ax, "".~r0+64(sp)
movq    $4835703278458516699, cx
imulq   cx
sarq    $18, dx
movq    "".~r0+64(sp), cx
sarq    $63, cx
subq    cx, dx
movq    "".start+72(sp), cx
subq    cx, dx
movq    dx, ""..autotmp_29+104(sp)
movq    "".count+88(sp), cx
movq    cx, ""..autotmp_30+96(sp)
xorps   x0, x0
movups  x0, ""..autotmp_28+184(sp)
movups  x0, ""..autotmp_28+200(sp)
leaq    type.int64(sb), cx
movq    cx, (sp)
leaq    ""..autotmp_29+104(sp), cx
movq    cx, 8(sp)
pcdata  $0, $3
call    runtime.convt2e64(sb)
movq    16(sp), cx
movq    24(sp), dx
movq    cx, ""..autotmp_28+184(sp)
movq    dx, ""..autotmp_28+192(sp)
leaq    type.int(sb), cx
movq    cx, (sp)
leaq    ""..autotmp_30+96(sp), cx
movq    cx, 8(sp)
pcdata  $0, $3
call    runtime.convt2e64(sb)
movq    24(sp), cx
movq    16(sp), dx
movq    dx, ""..autotmp_28+200(sp)
movq    cx, ""..autotmp_28+208(sp)
leaq    go.string."total time took: %d to get at count: %d\n"(sb), cx
movq    cx, (sp)
movq    $40, 8(sp)
leaq    ""..autotmp_28+184(sp), cx
movq    cx, 16(sp)
movq    $2, 24(sp)
movq    $2, 32(sp)
pcdata  $0, $3
call    fmt.printf(sb)
movq    216(sp), bp
addq    $224, sp
ret
movq    time.t·2+144(sp), bx
movq    cx, dx
movq    bx, cx
jmp     501
movq    time.t·2+168(sp), si
movq    cx, dx
movq    $59453308800, bx
movq    si, cx
jmp     144
nop
pcdata  $0, $-1
call    runtime.morestack_noctxt(sb)
jmp     0
text    "".init(sb), $8-0
movq    (tls), cx
cmpq    sp, 16(cx)
jls     89
subq    $8, sp
movq    bp, (sp)
leaq    (sp), bp
funcdata        $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(sb)
funcdata        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(sb)
movblzx "".initdone·(sb), ax
cmpb    al, $1
jls     47
movq    (sp), bp
addq    $8, sp
ret
jne     56
pcdata  $0, $0
call    runtime.throwinit(sb)
undef
movb    $1, "".initdone·(sb)
pcdata  $0, $0
call    fmt.init(sb)
pcdata  $0, $0
call    time.init(sb)
movb    $2, "".initdone·(sb)
movq    (sp), bp
addq    $8, sp
ret
nop
pcdata  $0, $-1
call    runtime.morestack_noctxt(sb)
jmp     0
text    type..hash.[2]interface {}(sb), dupok, $40-24
movq    (tls), cx
cmpq    sp, 16(cx)
jls     103
subq    $40, sp
movq    bp, 32(sp)
leaq    32(sp), bp
funcdata        $0, gclocals·d4dc2f11db048877dbc0f60a22b4adb3(sb)
funcdata        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(sb)
xorl    ax, ax
movq    "".h+56(sp), cx
jmp     82
movq    ax, "".i+24(sp)
shlq    $4, ax
movq    "".p+48(sp), bx
addq    bx, ax
movq    ax, (sp)
movq    cx, 8(sp)
pcdata  $0, $0
call    runtime.nilinterhash(sb)
movq    16(sp), cx
movq    "".i+24(sp), ax
incq    ax
cmpq    ax, $2
jlt     38
movq    cx, "".~r2+64(sp)
movq    32(sp), bp
addq    $40, sp
ret
nop
pcdata  $0, $-1
call    runtime.morestack_noctxt(sb)
jmp     0
text    type..eq.[2]interface {}(sb), dupok, $48-24
movq    (tls), cx
cmpq    sp, 16(cx)
jls     155
subq    $48, sp
movq    bp, 40(sp)
leaq    40(sp), bp
funcdata        $0, gclocals·8f9cec06d1ae35cc9900c511c5e4bdab(sb)
funcdata        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(sb)
xorl    ax, ax
jmp     46
movq    ""..autotmp_8+32(sp), cx
leaq    1(cx), ax
cmpq    ax, $2
jge     140
movq    ax, cx
shlq    $4, ax
movq    "".p+56(sp), dx
movq    8(ax)(dx*1), bx
movq    (ax)(dx*1), si
movq    "".q+64(sp), di
movq    8(ax)(di*1), r8
movq    (ax)(di*1), ax
cmpq    si, ax
jne     125
movq    cx, ""..autotmp_8+32(sp)
movq    si, (sp)
movq    bx, 8(sp)
movq    r8, 16(sp)
pcdata  $0, $0
call    runtime.efaceeq(sb)
movblzx 24(sp), ax
testb   al, al
jne     37
movb    $0, "".~r2+72(sp)
movq    40(sp), bp
addq    $48, sp
ret
movb    $1, "".~r2+72(sp)
movq    40(sp), bp
addq    $48, sp
ret
nop
pcdata  $0, $-1
call    runtime.morestack_noctxt(sb)
jmp     0

我不知道其中大部分在做什么。我只能希望其中大部分是某种 gc 代码。我查找了如何启用 go 编译器的优化,我所能找到的只是如何禁用优化。

相比之下,我在 c++ 中查看了类似的函数

#include <cstdio>
#include <chrono>
#include <cinttypes>

using namespace std::chrono;


milliseconds getms()
{
    return duration_cast< milliseconds >(
        system_clock::now().time_since_epoch()
    );
}

int main()
{
    int count = 0;
    milliseconds millis = getms();

    for(int i = 0; i < 10; ++i)
    {
        printf("on step %d\n", i);
        for(int j = 0; j < 2000000000; ++j)
        {
            ++count;
        }
    }

    milliseconds time = getms() - millis;

    printf("total time took: %" prid64 " to get at count: %d\n", time.count(), count);
}

未经优化编译为(编译器x86-64 clang(trunk(可能是6.0.0),标志:-std=c++0x -o0):

main:                                   # @main
        push    rbp
        mov     rbp, rsp
        sub     rsp, 48
        mov     dword ptr [rbp - 4], 0
        mov     dword ptr [rbp - 8], 0
        call    getms()
        mov     qword ptr [rbp - 16], rax
        mov     dword ptr [rbp - 20], 0
.lbb3_1:                                # =>this loop header: depth=1
        cmp     dword ptr [rbp - 20], 10
        jge     .lbb3_8
        mov     esi, dword ptr [rbp - 20]
        movabs  rdi, offset .l.str
        mov     al, 0
        call    printf
        mov     dword ptr [rbp - 24], 0
        mov     dword ptr [rbp - 44], eax # 4-byte spill
.lbb3_3:                                #   parent loop bb3_1 depth=1
        cmp     dword ptr [rbp - 24], 2000000000
        jge     .lbb3_6
        mov     eax, dword ptr [rbp - 8]
        add     eax, 1
        mov     dword ptr [rbp - 8], eax
        mov     eax, dword ptr [rbp - 24]
        add     eax, 1
        mov     dword ptr [rbp - 24], eax
        jmp     .lbb3_3
.lbb3_6:                                #   in loop: header=bb3_1 depth=1
        jmp     .lbb3_7
.lbb3_7:                                #   in loop: header=bb3_1 depth=1
        mov     eax, dword ptr [rbp - 20]
        add     eax, 1
        mov     dword ptr [rbp - 20], eax
        jmp     .lbb3_1
.lbb3_8:
        call    getms()
        mov     qword ptr [rbp - 40], rax
        lea     rdi, [rbp - 40]
        lea     rsi, [rbp - 16]
        call    std::common_type<std::chrono::duration<long, std::ratio<1l, 1000l> >, std::chrono::duration<long, std::ratio<1l, 1000l> > >::type std::chrono::operator-<long, std::ratio<1l, 1000l>, long, std::ratio<1l, 1000l> >(std::chrono::duration<long, std::ratio<1l, 1000l> > const&, std::chrono::duration<long, std::ratio<1l, 1000l> > const&)
        mov     qword ptr [rbp - 32], rax
        lea     rdi, [rbp - 32]
        call    std::chrono::duration<long, std::ratio<1l, 1000l> >::count() const
        mov     edx, dword ptr [rbp - 8]
        movabs  rdi, offset .l.str.1
        mov     rsi, rax
        mov     al, 0
        call    printf
        mov     edx, dword ptr [rbp - 4]
        mov     dword ptr [rbp - 48], eax # 4-byte spill
        mov     eax, edx
        add     rsp, 48
        pop     rbp
        ret
.l.str:
        .asciz  "on step %d\n"

.l.str.1:
        .asciz  "total time took: %ld to get at count: %d\n"

实际上还有很多代码,但它只是 chrono 实现,在优化后的代码中它只是一个库函数调用。我还删除了 getms 的实现,因为它主要是一个包装方法。

通过 o1(大小)优化,这会变成:

main:                                   # @main
        push    rbx
        sub     rsp, 32
        call    getms()
        mov     qword ptr [rsp + 24], rax
        xor     ebx, ebx
.lbb3_1:                                # =>this inner loop header: depth=1
        mov     edi, offset .l.str
        xor     eax, eax
        mov     esi, ebx
        call    printf
        add     ebx, 1
        cmp     ebx, 10
        jne     .lbb3_1
        call    getms()
        mov     qword ptr [rsp + 8], rax
        lea     rdi, [rsp + 8]
        lea     rsi, [rsp + 24]
        call    std::common_type<std::chrono::duration<long, std::ratio<1l, 1000l> >, std::chrono::duration<long, std::ratio<1l, 1000l> > >::type std::chrono::operator-<long, std::ratio<1l, 1000l>, long, std::ratio<1l, 1000l> >(std::chrono::duration<long, std::ratio<1l, 1000l> > const&, std::chrono::duration<long, std::ratio<1l, 1000l> > const&)
        mov     qword ptr [rsp + 16], rax
        lea     rdi, [rsp + 16]
        call    std::chrono::duration<long, std::ratio<1l, 1000l> >::count() const
        mov     rcx, rax
        mov     edi, offset .l.str.1
        mov     edx, -1474836480
        xor     eax, eax
        mov     rsi, rcx
        call    printf
        xor     eax, eax
        add     rsp, 32
        pop     rbx
        ret
.l.str:
        .asciz  "on step %d\n"

.l.str.1:
        .asciz  "total time took: %ld to get at count: %d\n"

o2(速度)和 o3(最大)优化本质上归结为展开的外循环(仅用于打印语句)和预先计算的计数值。

这主要显示了 go 生成的糟糕代码以及 c++ 中发生的一些优化。但这些都没有准确显示 java 字节码包含什么内容,或者如果运行足够多的时间,jit 会生成什么内容。这是 java 字节码:

public static void countToTwentyBillion();
    Code:

0: lconst_0

1: lstore_0

2: invokestatic  #2
// Method java/lang/System.currentTimeMillis:()J

5: lstore_2

6: iconst_0

7: istore
4

9: iload
4

11: bipush
10

13: if_icmpge
68

16: getstatic
#3
// Field java/lang/System.out:Ljava/io/PrintStream;

19: new
#4
// class java/lang/StringBuilder

22: dup

23: invokespecial #5
// Method java/lang/StringBuilder.'<init>':()V

26: ldc
#6
// String On step

28: invokevirtual #7
// Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;

31: iload
4

33: invokevirtual #8
// Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;

36: invokevirtual #9
// Method java/lang/StringBuilder.toString:()Ljava/lang/String;

39: invokevirtual #10
// Method java/io/PrintStream.println:(Ljava/lang/String;)V

42: iconst_0

43: istore
5

45: iload
5

47: ldc
#11
// int 2000000000

49: if_icmpge
62

52: lload_0

53: lconst_1

54: ladd

55: lstore_0

56: iinc
5, 1

59: goto
45

62: iinc
4, 1

65: goto
9

68: invokestatic  #2
// Method java/lang/System.currentTimeMillis:()J

71: lstore
4

73: getstatic
#3
// Field java/lang/System.out:Ljava/io/PrintStream;

76: new
#4
// class java/lang/StringBuilder

79: dup

80: invokespecial #5
// Method java/lang/StringBuilder.'<init>':()V

83: ldc
#12
// String Total time took:

85: invokevirtual #7
// Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;

88: lload
4

90: lload_2

91: lsub

92: invokevirtual #13
// Method java/lang/StringBuilder.append:(J)Ljava/lang/StringBuilder;

95: ldc
#14
// String  ms to get at count:

97: invokevirtual #7
// Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;

100: lload_0

101: invokevirtual #13
// Method java/lang/StringBuilder.append:(J)Ljava/lang/StringBuilder;

104: invokevirtual #9
// Method java/lang/StringBuilder.toString:()Ljava/lang/String;

107: invokevirtual #10
// Method java/io/PrintStream.println:(Ljava/lang/String;)V

110: return

不幸的是,目前我不想编译 hsdis 和 jit 代码,但它可能最终看起来像一些 c++ 示例。根据我对 jit 的了解,它可能能够预先计算计数值。但这段代码有点复杂(就循环而言),这可能会使快速 jit 优化变得更加困难。

以上就是《为什么此代码在 Go 中与 Java 中运行需要更长的时间》的详细内容,更多关于的资料请关注golang学习网公众号!

版本声明
本文转载于:stackoverflow 如有侵犯,请联系study_golang@163.com删除
在迭代映射的同时并发修改映射时,如何使用 RWMutex在迭代映射的同时并发修改映射时,如何使用 RWMutex
上一篇
在迭代映射的同时并发修改映射时,如何使用 RWMutex
Go http.FileServer 不提供所有静态内容
下一篇
Go http.FileServer 不提供所有静态内容
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • ChatExcel酷表:告别Excel难题,北大团队AI助手助您轻松处理数据
    ChatExcel酷表
    ChatExcel酷表是由北京大学团队打造的Excel聊天机器人,用自然语言操控表格,简化数据处理,告别繁琐操作,提升工作效率!适用于学生、上班族及政府人员。
    3204次使用
  • Any绘本:开源免费AI绘本创作工具深度解析
    Any绘本
    探索Any绘本(anypicturebook.com/zh),一款开源免费的AI绘本创作工具,基于Google Gemini与Flux AI模型,让您轻松创作个性化绘本。适用于家庭、教育、创作等多种场景,零门槛,高自由度,技术透明,本地可控。
    3416次使用
  • 可赞AI:AI驱动办公可视化智能工具,一键高效生成文档图表脑图
    可赞AI
    可赞AI,AI驱动的办公可视化智能工具,助您轻松实现文本与可视化元素高效转化。无论是智能文档生成、多格式文本解析,还是一键生成专业图表、脑图、知识卡片,可赞AI都能让信息处理更清晰高效。覆盖数据汇报、会议纪要、内容营销等全场景,大幅提升办公效率,降低专业门槛,是您提升工作效率的得力助手。
    3446次使用
  • 星月写作:AI网文创作神器,助力爆款小说速成
    星月写作
    星月写作是国内首款聚焦中文网络小说创作的AI辅助工具,解决网文作者从构思到变现的全流程痛点。AI扫榜、专属模板、全链路适配,助力新人快速上手,资深作者效率倍增。
    4555次使用
  • MagicLight.ai:叙事驱动AI动画视频创作平台 | 高效生成专业级故事动画
    MagicLight
    MagicLight.ai是全球首款叙事驱动型AI动画视频创作平台,专注于解决从故事想法到完整动画的全流程痛点。它通过自研AI模型,保障角色、风格、场景高度一致性,让零动画经验者也能高效产出专业级叙事内容。广泛适用于独立创作者、动画工作室、教育机构及企业营销,助您轻松实现创意落地与商业化。
    3824次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码