轻松测量经过的时间
- 2024-09-30 14:02:00
- admin 原创
- 115
问题描述:
我正在尝试使用time()来测量我的程序的各个点。
我不明白的是为什么前后的值相同?我知道这不是分析我的程序的最佳方式,我只是想看看某件事需要多长时间。
printf("**MyProgram::before time= %ld
", time(NULL));
doSomthing();
doSomthingLong();
printf("**MyProgram::after time= %ld
", time(NULL));
我尝试过:
struct timeval diff, startTV, endTV;
gettimeofday(&startTV, NULL);
doSomething();
doSomethingLong();
gettimeofday(&endTV, NULL);
timersub(&endTV, &startTV, &diff);
printf("**time taken = %ld %ld
", diff.tv_sec, diff.tv_usec);
我如何读取结果**time taken = 0 26339
?这是否意味着 26,339 纳秒 = 26.3 毫秒?
那**time taken = 4 45025
,这是否意味着 4 秒和 25 毫秒?
解决方案 1:
//***C++11 Style:***
#include <chrono>
std::chrono::steady_clock::time_point begin = std::chrono::steady_clock::now();
std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now();
std::cout << "Time difference = " << std::chrono::duration_cast<std::chrono::microseconds>(end - begin).count() << "[µs]" << std::endl;
std::cout << "Time difference = " << std::chrono::duration_cast<std::chrono::nanoseconds> (end - begin).count() << "[ns]" << std::endl;
解决方案 2:
0——达美航空
使用 delta 函数计算时间差:
auto start = std::chrono::steady_clock::now();
std::cout << "Elapsed(ms)=" << since(start).count() << std::endl;
since
接受任何时间点并产生任何持续时间(默认为毫秒)。它定义为:
template <
class result_t = std::chrono::milliseconds,
class clock_t = std::chrono::steady_clock,
class duration_t = std::chrono::milliseconds
>
auto since(std::chrono::time_point<clock_t, duration_t> const& start)
{
return std::chrono::duration_cast<result_t>(clock_t::now() - start);
}
1 - 计时器
使用基于以下内容的计时器std::chrono
:
Timer clock; // Timer<milliseconds, steady_clock>
clock.tick();
/* code you want to measure */
clock.tock();
cout << "Run time = " << clock.duration().count() << " ms
";
Timer
定义为:
template <class DT = std::chrono::milliseconds,
class ClockT = std::chrono::steady_clock>
class Timer
{
using timep_t = typename ClockT::time_point;
timep_t _start = ClockT::now(), _end = {};
public:
void tick() {
_end = timep_t{};
_start = ClockT::now();
}
void tock() { _end = ClockT::now(); }
template <class T = DT>
auto duration() const {
gsl_Expects(_end != timep_t{} && "toc before reporting");
return std::chrono::duration_cast<T>(_end - _start);
}
};
正如Howard Hinnant指出的那样,我们使用持续时间来保留在chrono
类型系统中并执行诸如平均或比较之类的操作(例如,这里这意味着使用std::chrono::milliseconds
)。当我们只进行 IO 时,我们使用count()
或持续时间的滴答(例如,这里是毫秒数)。
2 - 仪器仪表
任何可调用函数(函数、函数对象、lambda 等)都可用于基准测试。假设您有一个F
可调用的函数arg1,arg2
,可以使用以下方法:
cout << "F runtime=" << measure<>::duration(F, arg1, arg2).count() << "ms";
measure
定义为:
template <class TimeT = std::chrono::milliseconds,
class ClockT = std::chrono::steady_clock>
struct measure
{
template<class F, class ...Args>
static auto duration(F&& func, Args&&... args)
{
auto start = ClockT::now();
std::invoke(std::forward<F>(func), std::forward<Args>(args)...);
return std::chrono::duration_cast<TimeT>(ClockT::now()-start);
}
};
如 (1) 中所述,使用 w/o 持续时间.count()
对于想要在 I/O 之前对一系列持续时间进行后处理的客户端最有用,例如平均值:
auto avg = (measure<>::duration(func) + measure<>::duration(func)) / 2;
std::cout << "Average run time " << avg.count() << " ms
";
+这就是转发函数调用的原因。
+完整代码可以在这里找到
+我尝试基于 chrono构建基准测试框架,记录在这里
+旧演示
解决方案 3:
#include <ctime>
void f() {
using namespace std;
clock_t begin = clock();
code_to_time();
clock_t end = clock();
double elapsed_secs = double(end - begin) / CLOCKS_PER_SEC;
}
该time()
函数只能精确到秒,但CLOCKS_PER_SEC
秒内有“时钟”。这是一种简单、便携的测量方法,尽管它过于简单。
解决方案 4:
从你的问题中我可以看出,你似乎想知道执行某段代码后经过的时间。我想你会很乐意以秒为单位查看结果。如果是这样,请尝试使用difftime()
如下所示的函数。希望这能解决你的问题。
#include <time.h>
#include <stdio.h>
time_t start,end;
time (&start);
.
.
.
<your code>
.
.
.
time (&end);
double dif = difftime (end,start);
printf ("Elasped time is %.2lf seconds.", dif );
解决方案 5:
仅限 Windows:(Linux 标签是在我发布此答案后添加的)
您可以使用GetTickCount()来获取自系统启动以来经过的毫秒数。
long int before = GetTickCount();
// Perform time-consuming operation
long int after = GetTickCount();
解决方案 6:
struct profiler
{
std::string name;
std::chrono::high_resolution_clock::time_point p;
profiler(std::string const &n) :
name(n), p(std::chrono::high_resolution_clock::now()) { }
~profiler()
{
using dura = std::chrono::duration<double>;
auto d = std::chrono::high_resolution_clock::now() - p;
std::cout << name << ": "
<< std::chrono::duration_cast<dura>(d).count()
<< std::endl;
}
};
#define PROFILE_BLOCK(pbn) profiler _pfinstance(pbn)
用法如下::
{
PROFILE_BLOCK("Some time");
// your code or function
}
这与 RAII 的范围类似
注意这不是我的,但我认为它与此相关
解决方案 7:
time(NULL)
返回自 1970 年 1 月 1 日 00:00 (纪元)以来经过的秒数。因此,两个值之间的差值就是处理所花费的秒数。
int t0 = time(NULL);
doSomthing();
doSomthingLong();
int t1 = time(NULL);
printf ("time = %d secs
", t1 - t0);
您可以使用 获得更精细的结果getttimeofday()
,它以秒为单位返回当前时间,time()
并且也以微秒为单位。
解决方案 8:
time(NULL) 函数将返回自 1970 年 1 月 1 日 00:00 以来经过的秒数。而且由于该函数在程序中是在不同时间调用的,因此它
在 C++ 中始终是不同的时间
解决方案 9:
#include<time.h> // for clock
#include<math.h> // for fmod
#include<cstdlib> //for system
#include <stdio.h> //for delay
using namespace std;
int main()
{
clock_t t1,t2;
t1=clock(); // first time capture
// Now your time spanning loop or code goes here
// i am first trying to display time elapsed every time loop runs
int ddays=0; // d prefix is just to say that this variable will be used for display
int dhh=0;
int dmm=0;
int dss=0;
int loopcount = 1000 ; // just for demo your loop will be different of course
for(float count=1;count<loopcount;count++)
{
t2=clock(); // we get the time now
float difference= (((float)t2)-((float)t1)); // gives the time elapsed since t1 in milliseconds
// now get the time elapsed in seconds
float seconds = difference/1000; // float value of seconds
if (seconds<(60*60*24)) // a day is not over
{
dss = fmod(seconds,60); // the remainder is seconds to be displayed
float minutes= seconds/60; // the total minutes in float
dmm= fmod(minutes,60); // the remainder are minutes to be displayed
float hours= minutes/60; // the total hours in float
dhh= hours; // the hours to be displayed
ddays=0;
}
else // we have reached the counting of days
{
float days = seconds/(24*60*60);
ddays = (int)(days);
float minutes= seconds/60; // the total minutes in float
dmm= fmod(minutes,60); // the rmainder are minutes to be displayed
float hours= minutes/60; // the total hours in float
dhh= fmod (hours,24); // the hours to be displayed
}
cout<<"Count Is : "<<count<<"Time Elapsed : "<<ddays<<" Days "<<dhh<<" hrs "<<dmm<<" mins "<<dss<<" secs";
// the actual working code here,I have just put a delay function
delay(1000);
system("cls");
} // end for loop
}// end of main
解决方案 10:
第二个程序打印的值是秒和微秒。
0 26339 = 0.026'339 s = 26339 µs
4 45025 = 4.045'025 s = 4045025 µs
解决方案 11:
#include <ctime>
#include <cstdio>
#include <iostream>
#include <chrono>
#include <sys/time.h>
using namespace std;
using namespace std::chrono;
void f1()
{
high_resolution_clock::time_point t1 = high_resolution_clock::now();
high_resolution_clock::time_point t2 = high_resolution_clock::now();
double dif = duration_cast<nanoseconds>( t2 - t1 ).count();
printf ("Elasped time is %lf nanoseconds.
", dif );
}
void f2()
{
timespec ts1,ts2;
clock_gettime(CLOCK_REALTIME, &ts1);
clock_gettime(CLOCK_REALTIME, &ts2);
double dif = double( ts2.tv_nsec - ts1.tv_nsec );
printf ("Elasped time is %lf nanoseconds.
", dif );
}
void f3()
{
struct timeval t1,t0;
gettimeofday(&t0, 0);
gettimeofday(&t1, 0);
double dif = double( (t1.tv_usec-t0.tv_usec)*1000);
printf ("Elasped time is %lf nanoseconds.
", dif );
}
void f4()
{
high_resolution_clock::time_point t1 , t2;
double diff = 0;
t1 = high_resolution_clock::now() ;
for(int i = 1; i <= 10 ; i++)
{
t2 = high_resolution_clock::now() ;
diff+= duration_cast<nanoseconds>( t2 - t1 ).count();
t1 = t2;
}
printf ("high_resolution_clock:: Elasped time is %lf nanoseconds.
", diff/10 );
}
void f5()
{
timespec ts1,ts2;
double diff = 0;
clock_gettime(CLOCK_REALTIME, &ts1);
for(int i = 1; i <= 10 ; i++)
{
clock_gettime(CLOCK_REALTIME, &ts2);
diff+= double( ts2.tv_nsec - ts1.tv_nsec );
ts1 = ts2;
}
printf ("clock_gettime:: Elasped time is %lf nanoseconds.
", diff/10 );
}
void f6()
{
struct timeval t1,t2;
double diff = 0;
gettimeofday(&t1, 0);
for(int i = 1; i <= 10 ; i++)
{
gettimeofday(&t2, 0);
diff+= double( (t2.tv_usec-t1.tv_usec)*1000);
t1 = t2;
}
printf ("gettimeofday:: Elasped time is %lf nanoseconds.
", diff/10 );
}
int main()
{
// f1();
// f2();
// f3();
f6();
f4();
f5();
return 0;
}
解决方案 12:
C++ std::chrono 具有跨平台的明显优势。然而,与 POSIX clock_gettime() 相比,它也带来了相当大的开销。在我的 Linux 机器上,所有std::chrono::xxx_clock::now()
版本的性能大致相同:
std::chrono::system_clock::now()
std::chrono::steady_clock::now()
std::chrono::high_resolution_clock::now()
虽然 POSIXclock_gettime(CLOCK_MONOTONIC, &time)
应该相同steady_clock::now()
,但是速度却快了 3 倍多!
为了完整性,这是我的测试。
#include <stdio.h>
#include <chrono>
#include <ctime>
void print_timediff(const char* prefix, const struct timespec& start, const
struct timespec& end)
{
double milliseconds = end.tv_nsec >= start.tv_nsec
? (end.tv_nsec - start.tv_nsec) / 1e6 + (end.tv_sec - start.tv_sec) * 1e3
: (start.tv_nsec - end.tv_nsec) / 1e6 + (end.tv_sec - start.tv_sec - 1) * 1e3;
printf("%s: %lf milliseconds
", prefix, milliseconds);
}
int main()
{
int i, n = 1000000;
struct timespec start, end;
// Test stopwatch
clock_gettime(CLOCK_MONOTONIC, &start);
for (i = 0; i < n; ++i) {
struct timespec dummy;
clock_gettime(CLOCK_MONOTONIC, &dummy);
}
clock_gettime(CLOCK_MONOTONIC, &end);
print_timediff("clock_gettime", start, end);
// Test chrono system_clock
clock_gettime(CLOCK_MONOTONIC, &start);
for (i = 0; i < n; ++i)
auto dummy = std::chrono::system_clock::now();
clock_gettime(CLOCK_MONOTONIC, &end);
print_timediff("chrono::system_clock::now", start, end);
// Test chrono steady_clock
clock_gettime(CLOCK_MONOTONIC, &start);
for (i = 0; i < n; ++i)
auto dummy = std::chrono::steady_clock::now();
clock_gettime(CLOCK_MONOTONIC, &end);
print_timediff("chrono::steady_clock::now", start, end);
// Test chrono high_resolution_clock
clock_gettime(CLOCK_MONOTONIC, &start);
for (i = 0; i < n; ++i)
auto dummy = std::chrono::high_resolution_clock::now();
clock_gettime(CLOCK_MONOTONIC, &end);
print_timediff("chrono::high_resolution_clock::now", start, end);
return 0;
}
这是我使用 gcc7.2 -O3 编译时得到的输出:
clock_gettime: 24.484926 milliseconds
chrono::system_clock::now: 85.142108 milliseconds
chrono::steady_clock::now: 87.295347 milliseconds
chrono::high_resolution_clock::now: 84.437838 milliseconds
解决方案 13:
函数time(NULL)
调用将返回自 epoc:1970 年 1 月 1 日以来经过的秒数。也许您想要做的是取两个时间戳之间的差值:
size_t start = time(NULL);
doSomthing();
doSomthingLong();
printf ("**MyProgram::time elapsed= %lds
", time(NULL) - start);
解决方案 14:
在 Linux 上,clock_gettime() 是一个不错的选择。您必须链接实时库 (-lrt)。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <time.h>
#define BILLION 1000000000L;
int main( int argc, char **argv )
{
struct timespec start, stop;
double accum;
if( clock_gettime( CLOCK_REALTIME, &start) == -1 ) {
perror( "clock gettime" );
exit( EXIT_FAILURE );
}
system( argv[1] );
if( clock_gettime( CLOCK_REALTIME, &stop) == -1 ) {
perror( "clock gettime" );
exit( EXIT_FAILURE );
}
accum = ( stop.tv_sec - start.tv_sec )
+ ( stop.tv_nsec - start.tv_nsec )
/ BILLION;
printf( "%lf
", accum );
return( EXIT_SUCCESS );
}
解决方案 15:
正如其他人已经指出的那样,C 标准库中的 time() 函数的精度不超过一秒。唯一可以提供更好精度的完全可移植 C 函数似乎是 clock(),但它测量的是处理器时间而不是挂钟时间。如果一个人满足于将自己限制在 POSIX 平台(例如 Linux),那么 clock_gettime() 函数是一个不错的选择。
自 C++11 以来,有更好的计时工具可用,它们以在不同的编译器和操作系统之间高度可移植的形式提供更好的分辨率。同样,boost::datetime 库提供了良好的高分辨率计时类,这些类应该具有高度可移植性。
使用这些工具的一大挑战是查询系统时钟带来的时间延迟。通过对clock_gettime()、boost::datetime和std::chrono的实验,这种延迟很容易达到微秒级。因此,在测量代码任何部分的持续时间时,您需要考虑到大约这个大小的测量误差,或者尝试以某种方式纠正零误差。理想情况下,您可能希望收集函数所用时间的多个测量值,并计算多次运行所用时间的平均值或最大/最小值。
为了解决所有这些可移植性和统计信息收集问题,我一直在开发Github上提供的 cxx-rtimers 库,该库试图提供一个简单的 API,用于计时 C++ 代码块、计算零错误以及报告代码中嵌入的多个计时器的统计信息。如果您有 C++11 编译器,您只需#include <rtimers/cxx11.hpp>
,并使用类似以下内容:
void expensiveFunction() {
static rtimers::cxx11::DefaultTimer timer("expensiveFunc");
auto scopedStartStop = timer.scopedStart();
// Do something costly...
}
在程序退出时,您将获得写入 std::cerr 的时间统计摘要,例如:
Timer(expensiveFunc): <t> = 6.65289us, std = 3.91685us, 3.842us <= t <= 63.257us (n=731)
显示平均时间、标准差、上限和下限以及调用该函数的次数。
如果您想使用 Linux 特定的计时函数,您可以#include <rtimers/posix.hpp>
,或者如果您有 Boost 库但使用的是较旧的 C++ 编译器,您可以#include <rtimers/boost.hpp>
。这些计时器类的某些版本还可以从多个线程中收集统计计时信息。还有一些方法允许您估计与系统时钟的两个连续查询相关的零误差。
解决方案 16:
该函数将内部访问系统时钟,这就是每次调用时返回不同值的原因。一般来说,非函数式语言的函数中可能存在许多副作用和隐藏状态,仅通过查看函数的名称和参数是无法发现的。
解决方案 17:
从图中可以看出,tv_sec 存储的是经过的秒数,而 tv_usec 存储的是经过的微秒数,而且它们之间不是相互转换的,因此必须将它们转换为合适的单位并相加才能得到总经过的时间。
struct timeval startTV, endTV;
gettimeofday(&startTV, NULL);
doSomething();
doSomethingLong();
gettimeofday(&endTV, NULL);
printf("**time taken in microseconds = %ld
",
(endTV.tv_sec * 1e6 + endTV.tv_usec - (startTV.tv_sec * 1e6 + startTV.tv_usec))
);
解决方案 18:
我需要测量库中各个函数的执行时间。我不想用时间测量函数包装每个函数的每次调用,因为这样很丑陋,而且会加深调用堆栈。我也不想把计时器代码放在每个函数的顶部和底部,因为当函数可能提前退出或抛出异常时,它会造成混乱。所以我最终做的是制作一个使用其自身生命周期来测量时间的计时器。
通过这种方式,我只需在相关代码块(函数或任何范围)的开头实例化其中一个对象,然后允许实例析构函数在实例超出范围时测量自构造以来经过的时间,就可以测量代码块所花费的挂钟时间。您可以在此处找到完整的示例,但结构非常简单:
template <typename clock_t = std::chrono::steady_clock>
struct scoped_timer {
using duration_t = typename clock_t::duration;
const std::function<void(const duration_t&)> callback;
const std::chrono::time_point<clock_t> start;
scoped_timer(const std::function<void(const duration_t&)>& finished_callback) :
callback(finished_callback), start(clock_t::now()) { }
scoped_timer(std::function<void(const duration_t&)>&& finished_callback) :
callback(finished_callback), start(clock_t::now()) { }
~scoped_timer() { callback(clock_t::now() - start); }
};
当结构超出范围时,它将回调提供的函子,以便您可以对时间信息进行一些操作(打印或存储等等)。如果您需要执行更复杂的事情,您甚至可以使用std::bind
带有std::placeholders
更多参数的回调函数。
以下是使用它的简单示例:
void test(bool should_throw) {
scoped_timer<> t([](const scoped_timer<>::duration_t& elapsed) {
auto e = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(elapsed).count();
std::cout << "took " << e << "ms" << std::endl;
});
std::this_thread::sleep_for(std::chrono::seconds(1));
if (should_throw)
throw nullptr;
std::this_thread::sleep_for(std::chrono::seconds(1));
}
如果您想要更加慎重,您还可以使用new
和delete
明确地启动和停止计时器,而不依赖作用域来为您完成此操作。
解决方案 19:
它们是相同的,因为您的 doSomething 函数发生得比计时器的粒度更快。尝试:
printf ("**MyProgram::before time= %ld
", time(NULL));
for(i = 0; i < 1000; ++i) {
doSomthing();
doSomthingLong();
}
printf ("**MyProgram::after time= %ld
", time(NULL));
解决方案 20:
两个值相同的原因是您的长程序不需要那么长时间 - 不到一秒钟。您可以尝试在函数末尾添加一个长循环 (for (int i = 0; i < 100000000; i++) ; ) 以确保这是问题所在,然后我们可以从那里开始...
如果上述情况属实,您将需要找到不同的系统函数(我知道您使用 Linux,因此我无法帮助您找到函数名称)来更准确地测量时间。我确信 Linux 中有一个类似于 GetTickCount() 的函数,您只需找到它即可。
解决方案 21:
我通常使用以下内容:
#include <chrono>
#include <type_traits>
using perf_clock = std::conditional<
std::chrono::high_resolution_clock::is_steady,
std::chrono::high_resolution_clock,
std::chrono::steady_clock
>::type;
using floating_seconds = std::chrono::duration<double>;
template<class F, class... Args>
floating_seconds run_test(Func&& func, Args&&... args)
{
const auto t0 = perf_clock::now();
std::forward<Func>(func)(std::forward<Args>(args)...);
return floating_seconds(perf_clock::now() - t0);
}
它与@nikos-athanasiou 提出的相同,只是我避免使用非稳定时钟并使用浮动秒数作为持续时间。
解决方案 22:
Matlab
有味道!
tic
启动秒表计时器来测量性能。该函数记录执行 tic 命令时的内部时间。使用该toc
函数显示已用时间。
#include <iostream>
#include <ctime>
#include <thread>
using namespace std;
clock_t START_TIMER;
clock_t tic()
{
return START_TIMER = clock();
}
void toc(clock_t start = START_TIMER)
{
cout
<< "Elapsed time: "
<< (clock() - start) / (double)CLOCKS_PER_SEC << "s"
<< endl;
}
int main()
{
tic();
this_thread::sleep_for(2s);
toc();
return 0;
}
解决方案 23:
这是一个简单的类,它将打印在指定的持续时间单位内进入和超出范围之间的持续时间:
#include <chrono>
#include <iostream>
template <typename T>
class Benchmark
{
public:
Benchmark(std::string name) : start(std::chrono::steady_clock::now()), name(name) {}
~Benchmark()
{
auto end = std::chrono::steady_clock::now();
T duration = std::chrono::duration_cast<T>(end - start);
std::cout << "Bench \"" << name << "\" took: " << duration.count() << " units" << std::endl;
}
private:
std::string name;
std::chrono::time_point<std::chrono::steady_clock> start;
};
int main()
{
Benchmark<std::chrono::nanoseconds> bench("for loop");
for(int i = 0; i < 1001000; i++){}
}
使用示例:
int main()
{
Benchmark<std::chrono::nanoseconds> bench("for loop");
for(int i = 0; i < 100000; i++){}
}
输出:
Bench "for loop" took: 230656 units
解决方案 24:
回答 OP 的三个具体问题。
“我不明白的是为什么前后的值相同? ”
第一个问题和示例代码显示其time()
精度为 1 秒,因此答案必须是两个函数的执行时间少于 1 秒。但偶尔,如果两个计时器标记跨越一秒边界,它会(显然不合逻辑地)告知1 秒。
下一个示例使用gettimeofday()
填充此结构
struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
};
第二个问题是:“如何读取结果**time taken = 0 26339
?这是否意味着 26,339 纳秒 = 26.3 毫秒?”
我的第二个答案是所花费的时间为 0 秒 26339 微秒,即 0.026339 秒,这证实了第一个例子在不到 1 秒的时间内执行完成。
第三个问题是:“那呢**time taken = 4 45025
,这是否意味着4秒和25毫秒?”
我的第三个答案是,所花费的时间为4秒45025微秒,即4.045025秒,这表明OP改变了他之前计时的两个函数所执行的任务。
解决方案 25:
#include <ctime>
#include <functional>
using namespace std;
void f() {
clock_t begin = clock();
// ...code to measure time...
clock_t end = clock();
function<double(double, double)> convtime = [](clock_t begin, clock_t end)
{
return double(end - begin) / CLOCKS_PER_SEC;
};
printf("Elapsed time: %.2g sec
", convtime(begin, end));
}
与此处提供的示例类似,但具有附加转换功能+打印输出。
解决方案 26:
我已经创建了一个类来自动测量经过的时间,请检查此链接中的代码(c++11):https ://github.com/sonnt174/Common/blob/master/time_measure.h
如何使用 TimeMeasure 类的示例:
void test_time_measure(std::vector<int> arr) {
TimeMeasure<chrono::microseconds> time_mea; // create time measure obj
std::sort(begin(arr), end(arr));
}
- 2024年20款好用的项目管理软件推荐,项目管理提效的20个工具和技巧
- 2024年开源项目管理软件有哪些?推荐5款好用的项目管理工具
- 项目管理软件有哪些?推荐7款超好用的项目管理工具
- 项目管理软件哪个最好用?盘点推荐5款好用的项目管理工具
- 项目管理软件有哪些最好用?推荐6款好用的项目管理工具
- 项目管理软件有哪些,盘点推荐国内外超好用的7款项目管理工具
- 2024项目管理软件排行榜(10类常用的项目管理工具全推荐)
- 项目管理软件排行榜:2024年项目经理必备5款开源项目管理软件汇总
- 2024年常用的项目管理软件有哪些?推荐这10款国内外好用的项目管理工具
- 项目管理必备:盘点2024年13款好用的项目管理软件