注册 登录
编程论坛 VFP论坛

提高代码的运行速度

sdta 发布于 2021-03-31 20:44, 4338 次点击
电脑配置
只有本站会员才能查看附件,请 登录

VFP9 汉化版 7423
这是一门科目的考试成绩,保存在JMSS1字段中,共有8题(各有129403条记录),以逗号分隔,成绩不但有0分、3分,还有带小数位的成绩(如1.5分),见下图
只有本站会员才能查看附件,请 登录

2,2,2,2,0,3,3,3 分别代表每题得到的分数。
根据这台电脑的配置情况,获取这门科目8题的有关数据耗时4秒。
要求:1、获取每题分值对应的人数,效果图如下
只有本站会员才能查看附件,请 登录

      2、尽最大能力缩短程序运行的时间

附上运行代码及相关数据:
只有本站会员才能查看附件,请 登录

程序代码:
t1 = SECONDS()
CLOSE DATABASES
USE 选择\sp ALIAS aa
ln = OCCURS(",", jmss1) + 1 && 获取题目的数量
FOR lnj = 1 TO ln
    CREATE CURSOR fz (nfz n(4, 2), nsl I) && 保存分值的
    INDEX on nfz TAG fz
    SELECT VAL(GETWORDNUM(jmss1, lnj, ",")) nfz FROM aa INTO CURSOR temp
    SCAN
        IF SEEK(temp.nfz, "fz", "fz") = .T.
            REPLACE nsl WITH nsl + 1 IN fz
        ELSE
            INSERT INTO fz VALUES (temp.nfz, 1)
        ENDIF
    ENDSCAN
ENDFOR
MESSAGEBOX(SECONDS() - t1)
SELECT fz
BROWSE


38 回复
#2
sdta2021-04-01 07:58
如果不用临时表,用字符,时间可以缩短为3.3秒,于事无补。
#3
radiofan2021-04-01 08:54
有空试试
#4
gs25367856782021-04-01 10:29
能够写出代码解决问题已经不容易了,千万不用求全自责。
说真的,程序员的工作只是使得程序顺利运行,没有时间去优化代码。
#5
瓜瓜19902021-04-01 10:41
直接用group by不一样么,快不少
程序代码:
t1 = SECONDS()
CLOSE DATABASES
USE sp ALIAS aa
ln = OCCURS(",", jmss1) + 1 && 获取题目的数量
FOR lnj = 1 TO ln
    temp="temp"+"_"+TRANSFORM(m.lnj)
    SELECT VAL(getwordnum(jmss1,m.lnj,",") )nfz ,COUNT(*) as nrs FROM aa INTO CURSOR (temp) GROUP BY 1
ENDFOR
MESSAGEBOX(SECONDS() - t1)
BROWSE

只有本站会员才能查看附件,请 登录
只有本站会员才能查看附件,请 登录
#6
sdta2021-04-01 11:06
1 楼代码变更如下
程序代码:
T1 = SECONDS()
CLOSE DATABASES
USE 选择\sp ALIAS aa
ln = OCCURS(",", jmss1) + 1 && 获取题目的数量
FOR lnj = 1 TO ln
    CREATE CURSOR fz (nfz n(4, 2), nsl I) && 保存分值的
    INDEX on nfz TAG fz
    SELECT aa
    SCAN
        IF SEEK(VAL(GETWORDNUM(aa.jmss1, lnj, ",")), "fz", "fz") = .T.
            REPLACE nsl WITH nsl + 1 IN fz
        ELSE
            INSERT INTO fz VALUES (VAL(GETWORDNUM(aa.jmss1, lnj, ",")), 1)
        ENDIF
    ENDSCAN
ENDFOR
MESSAGEBOX(SECONDS() - T1)

发本帖的原意是,在不使用SQL命令查询的前提下,VFP命令得到结果的极限运行时间。
SUBSTR()与GETWORDNUM()在截取字符串时各有优势,如果字符串中子字符串的宽度是一样的(以分隔符分隔的10000个子字符串),GETWORDNUM()截取子字符串所需时间是SUBSTR()截取子字符串所需时间的60倍(至少);如果字符串中子字符串的宽度是不一样的(如本题中分值带有小数位),GETWORDNUM()的优势就明显了。
如:
2,2,2,2,0,1.5,3,3
2,2,2,2,0,3,1.5,3
2,2,2,2,0,3,3,1.5
因为你不知道带小数位的分值出现在那一题中,所以用SUBSTR()截取子字符串很麻烦。

[此贴子已经被作者于2021-4-1 11:17编辑过]

#7
sdta2021-04-01 11:09
以下是引用瓜瓜1990在2021-4-1 10:41:26的发言:

直接用group by不一样么,快不少
t1 = SECONDS()
CLOSE DATABASES
USE sp ALIAS aa
ln = OCCURS(",", jmss1) + 1 && 获取题目的数量
FOR lnj = 1 TO ln
    temp="temp"+"_"+TRANSFORM(m.lnj)
    SELECT VAL(getwordnum(jmss1,m.lnj,",") )nfz ,COUNT(*) as nrs FROM aa INTO CURSOR (temp) GROUP BY 1
ENDFOR
MESSAGEBOX(SECONDS() - t1)
BROWSE

谢谢瓜瓜1990
#8
mywisdom882021-04-01 11:49
数据量不够大,效果不明显的,才10W的数据,要100W去,估计很明显;因为各人的电脑配置不相同
#9
sdta2021-04-01 12:06
以下是引用mywisdom88在2021-4-1 11:49:08的发言:

数据量不够大,效果不明显的,才10W的数据,要100W去,估计很明显;因为各人的电脑配置不相同

代码运行时间以我的电脑为准
#10
mywisdom882021-04-01 12:58
只有本站会员才能查看附件,请 登录

我的晕死,190秒
只有本站会员才能查看附件,请 登录
#11
bccn2012032021-04-01 13:09
以下是引用mywisdom88在2021-4-1 12:58:55的发言:


我的晕死,190秒

是不是水土不服
两台电脑的配置那个好
#12
mywisdom882021-04-01 13:28
用5楼的,才0.6秒
#13
mywisdom882021-04-01 13:40
把程序放到C盘,变成144秒(C盘是固态盘,I是以前的机械盘)
把这个,去掉,就变成1.6秒,速度和写盘,有很大关系
        IF SEEK(VAL(GETWORDNUM(aa.jmss1, lnj, ",")), "fz", "fz") = .T.
      *      REPLACE nsl WITH nsl + 1 IN fz
        ELSE
      *     INSERT INTO fz VALUES (VAL(GETWORDNUM(aa.jmss1, lnj, ",")), 1)
        ENDIF
********************************


SET DEFAULT TO c:\abc
T1 = SECONDS()
CLOSE DATABASES
USE 选择\sp ALIAS aa
ln = OCCURS(",", jmss1) + 1 && 获取题目的数量
FOR lnj = 1 TO ln
    CREATE CURSOR fz (nfz n(4, 2), nsl I) && 保存分值的
    INDEX on nfz TAG fz
    SELECT aa
    SCAN
        IF SEEK(VAL(GETWORDNUM(aa.jmss1, lnj, ",")), "fz", "fz") = .T.
            REPLACE nsl WITH nsl + 1 IN fz
        ELSE
            INSERT INTO fz VALUES (VAL(GETWORDNUM(aa.jmss1, lnj, ",")), 1)
        ENDIF
    ENDSCAN
ENDFOR
MESSAGEBOX(SECONDS() - T1)
#14
schtg2021-04-01 13:52
向各位学习!
只有本站会员才能查看附件,请 登录


[此贴子已经被作者于2021-4-1 13:54编辑过]

#15
sdta2021-04-01 14:41
6楼的代码应该比5楼的代码运行慢
#16
sdta2021-04-01 14:51
以下是引用mywisdom88在2021-4-1 13:40:57的发言:

把程序放到C盘,变成144秒(C盘是固态盘,I是以前的机械盘)
把这个,去掉,就变成1.6秒,速度和写盘,有很大关系
        IF SEEK(VAL(GETWORDNUM(aa.jmss1, lnj, ",")), "fz", "fz") = .T.
      *      REPLACE nsl WITH nsl + 1 IN fz
        ELSE
      *     INSERT INTO fz VALUES (VAL(GETWORDNUM(aa.jmss1, lnj, ",")), 1)
        ENDIF
********************************


SET DEFAULT TO c:\abc
T1 = SECONDS()
CLOSE DATABASES
USE 选择\sp ALIAS aa
ln = OCCURS(",", jmss1) + 1 && 获取题目的数量
FOR lnj = 1 TO ln
    CREATE CURSOR fz (nfz n(4, 2), nsl I) && 保存分值的
    INDEX on nfz TAG fz
    SELECT aa
    SCAN
        IF SEEK(VAL(GETWORDNUM(aa.jmss1, lnj, ",")), "fz", "fz") = .T.
            REPLACE nsl WITH nsl + 1 IN fz
        ELSE
            INSERT INTO fz VALUES (VAL(GETWORDNUM(aa.jmss1, lnj, ",")), 1)
        ENDIF
    ENDSCAN
ENDFOR
MESSAGEBOX(SECONDS() - T1)

是不是说固态盘影响数据的写入
#17
厨师王德榜2021-04-01 17:15
我的代码,按照要求不用任何Select ... 语句 ,另辟蹊径用了字典对象,供大家参考:
程序代码:
IF NOT USED('sp') THEN
    USE "C:\sp.dbf" IN 0 EXCLUSIVE  &&  改成你的路径
ENDIF
LOCAL icnt as Integer ,ii as Integer
LOCAL ckey as String ,str1 as String
LOCAL time11 as Datetime ,time22 as Datetime

IF  USED('result1') THEN
    USE IN result1
ENDIF

LOCAL  oDic As Object
oDic = CreateObject("Scripting.Dictionary")
time11 = SECONDS()
SELECT sp
GO TOP
SCAN
    str1 = ALLTRIM(sp.jmss1)
    icnt = ALINES(arr2,str1,1,",")
    FOR ii =1 TO icnt
        ckey = LTRIM(STR(ii)) - "_" - arr2(ii)
        IF odic.Exists(ckey) THEN
            odic.Item(ckey) = odic.Item(ckey) + 1
        ELSE
            odic.Add( ckey,1) &&
        ENDIF     
    ENDFOR

ENDSCAN

DIMENSION arrKey(1) as String
arrKey =odic.Keys

CREATE CURSOR result1 (第几题 c(4) , 分值 c(4),次数 i )
FOR ii = 1 TO odic.Count  &&
*!*        tmp11 = LEFT(arrKey(ii), AT('_',arrKey(ii)) -1)
*!*        tmp22 = SUBSTR(arrKey(ii), AT('_',arrKey(ii)) +1)
*!*        tmp33 = odic.Item(arrKey(ii))
   
    INSERT INTO result1 (第几题  , 分值 ,次数  ) ;
        VALUES ( LEFT(arrKey(ii), AT('_',arrKey(ii)) -1),  ;
                SUBSTR(arrKey(ii), AT('_',arrKey(ii)) +1), ;
                odic.Item(arrKey(ii)) )
ENDFOR
time22 = SECONDS()
odic.RemoveAll
RELEASE oDic
SELECT result1
INDEX on 第几题 + 分值 TO 'C:\ind12'   &&  改成你的路径

BROWSE TITLE "总耗时" +  LTRIM(STR(time22 - time11 ,14,4 ))

在我的电脑上运行结果:
只有本站会员才能查看附件,请 登录

#18
mywisdom882021-04-01 17:21
回复 16楼 sdta
我电脑WIN7 64,8G内存
但不知道,为什么这么慢
1个120G固态硬盘,做C盘,其他的是,D,E,F,G,I是机械硬盘
#19
ycvf2021-04-01 17:44
这个函数GETWORDNUM(jmss1, lnj, ",")没见过,有知道的吗、
#20
sdta2021-04-01 17:46
以下是引用mywisdom88在2021-4-1 17:21:14的发言:

我电脑WIN7 64,8G内存
但不知道,为什么这么慢
1个120G固态硬盘,做C盘,其他的是,D,E,F,G,I是机械硬盘

对于硬件我是绝对外行
#21
sdta2021-04-01 17:48
以下是引用ycvf在2021-4-1 17:44:23的发言:

这个函数GETWORDNUM(jmss1, lnj, ",")没见过,有知道的吗、

VFP9 版本中的一个函数,大体意思:从一个字符串中返回指定单词。
#22
sdta2021-04-01 18:31
以下是引用厨师王德榜在2021-4-1 17:15:35的发言:

我的代码,按照要求不用任何Select ... 语句 ,另辟蹊径用了字典对象,供大家参考:
IF NOT USED('sp') THEN
    USE "C:\sp.dbf" IN 0 EXCLUSIVE  &&  改成你的路径
ENDIF
LOCAL icnt as Integer ,ii as Integer
LOCAL ckey as String ,str1 as String
LOCAL time11 as Datetime ,time22 as Datetime

IF  USED('result1') THEN
    USE IN result1
ENDIF

LOCAL  oDic As Object
oDic = CreateObject("Scripting.Dictionary")
time11 = SECONDS()
SELECT sp
GO TOP
SCAN
    str1 = ALLTRIM(sp.jmss1)
    icnt = ALINES(arr2,str1,1,",")
    FOR ii =1 TO icnt
        ckey = LTRIM(STR(ii)) - "_" - arr2(ii)
        IF odic.Exists(ckey) THEN
            odic.Item(ckey) = odic.Item(ckey) + 1
        ELSE
            odic.Add( ckey,1) &&
        ENDIF     
    ENDFOR

ENDSCAN

DIMENSION arrKey(1) as String
arrKey =odic.Keys

CREATE CURSOR result1 (第几题 c(4) , 分值 c(4),次数 i )
FOR ii = 1 TO odic.Count  &&
*!*        tmp11 = LEFT(arrKey(ii), AT('_',arrKey(ii)) -1)
*!*        tmp22 = SUBSTR(arrKey(ii), AT('_',arrKey(ii)) +1)
*!*        tmp33 = odic.Item(arrKey(ii))
   
    INSERT INTO result1 (第几题  , 分值 ,次数  ) ;
        VALUES ( LEFT(arrKey(ii), AT('_',arrKey(ii)) -1),  ;
                SUBSTR(arrKey(ii), AT('_',arrKey(ii)) +1), ;
                odic.Item(arrKey(ii)) )
ENDFOR
time22 = SECONDS()
odic.RemoveAll
RELEASE oDic
SELECT result1
INDEX on 第几题 + 分值 TO 'C:\ind12'   &&  改成你的路径

BROWSE TITLE "总耗时" +  LTRIM(STR(time22 - time11 ,14,4 ))

在我的电脑上运行结果:

独辟蹊径,别具一格,方法学习了,速度不理想。

[此贴子已经被作者于2021-4-1 18:34编辑过]

#23
schtg2021-04-01 18:39
回复 17楼 厨师王德榜
学习啦,谢谢!
#24
sdta2021-04-01 20:49
经过分析影响速度的主要原因是子字符串截取的速度上,在子字符串长度一样的情况下,SUBSTR()速度是最快的,但在本题子字符串的长度不一样的情况下SUBSTR()就用不上了,VFP9中的其它几个函数如:GETWORDNUM()、ALINES()等函数均不理想,只有另起炉灶了。
#25
吹水佬2021-04-01 21:07
SUBSTR() + AT() 就可以,但字符串不长,与GETWORDNUM()差别不大。ALINES()因要分配内存和读写操作会耗费一些。
只有本站会员才能查看附件,请 登录

程序代码:
DIMENSION az[100]
USE sp
t1 = SECONDS()
n = GETWORDCOUNT(jmss1, ",")
FOR i=1 TO n
    STORE 0 TO az
    SCAN
        j = (VAL(GETWORDNUM(jmss1,i,","))+1) * 10
        az[j] = az[j]+1
    ENDSCAN
ENDFOR
? SECONDS() - t1
FOR i=1 TO 100
    IF az[i] > 0
        ? i/10-1, az[i]
    ENDIF
ENDFOR
#26
吹水佬2021-04-01 21:16
只有本站会员才能查看附件,请 登录
#27
sdta2021-04-01 21:20
以下是引用吹水佬在2021-4-1 21:07:26的发言:

SUBSTR() + AT() 就可以,但字符串不长,与GETWORDNUM()差别不大。ALINES()因要分配内存和读写操作会耗费一些。

DIMENSION az[100]
USE sp
t1 = SECONDS()
n = GETWORDCOUNT(jmss1, ",")
FOR i=1 TO n
    STORE 0 TO az
    SCAN
        j = (VAL(GETWORDNUM(jmss1,i,","))+1) * 10
        az[j] = az[j]+1
    ENDSCAN
ENDFOR
? SECONDS() - t1

FOR i=1 TO 100
    IF az[i] > 0
        ? i/10-1, az[i]
    ENDIF
ENDFOR

吹版的代码在我的电脑上运行时间为1.2秒左右


吹版的代码精简效率高,我的又一个版本也是你这个数组思路,但是代码比你的多了不少行,判断也多,故而效率也低了不少,向吹版学习致敬!
程序代码:
T1 = SECONDS()
CLOSE DATABASES
USE 选择\sp ALIAS aa
ln = OCCURS(",", jmss1) + 1 && 获取题目的数量
FOR lnj = 1 TO ln
    lnCnt = 0
    RELEASE la
    LOCAL la[10, 2]
    SCAN
        lnfz = VAL(GETWORDNUM(jmss1, lnj, ",")) &&VAL(SUBSTR(jmss1, 2 *lnj - 1, 1))
        IF ASCAN(la, lnfz, 1, ALEN(la, 1), 1) = 0
            lnCnt = lnCnt + 1
            la[lnCnt, 1] = lnfz
            la[lnCnt, 2] = 1
        ELSE
            lnRow = (ASCAN(la, lnfz, 1, ALEN(la, 1), 1) + 1) / 2
            la[lnRow, 2] = la[lnRow, 2] + 1
        ENDIF
    ENDSCAN
ENDFOR
MESSAGEBOX(SECONDS() - T1)
FOR lnj = 1 TO ALEN(la, 1)
    IF ASCAN(la, .F., 1, ALEN(la, 1), 1) > 0
        lnCnt = (ASCAN(la, .F., 1, ALEN(la, 1), 1) + 1) / 2
        EXIT
    ENDIF
ENDFOR
DIMENSION la(lnCnt - 1, 2)
ASORT(la)
LIST MEMORY LIKE la && 最后一题的数组结果

我的这段代码运行时间为2.4秒左右

[此贴子已经被作者于2021-4-1 21:26编辑过]

#28
sdta2021-04-01 21:50
回复 26楼 吹水佬
结果完全正确
因为后期还要处理每题的其它几个指标:如平均分,最高分,满分率,零分率,有了分值及对应的人数,计算这几个指标的运行时间可以忽略不计。这是去年帮别人做的一个指标统计处理小程序,当时用的就是瓜瓜在前面贴出的SQL代码,最近闲来无事,把这个问题重新找出来了,看只用VFP命令(不用SQL查询)与SQL查询时间的差距有多大,现在看来运行时间差不多,大约3-5秒左右的误差。考试科目共7门,现在讨论的题目只是其中的一门,还有一门课程的题目有50多题,刚才运行了吹版的代码,运行时间在17秒左右。如果试题在10题内,每题运行时间在0.15秒,但是55题每题运行时间在0.3秒左右,这是什么原因?吹版能帮解释一下吗
#29
ycvf2021-04-01 21:57
回复 21楼 sdta
能举例说明吗?
#30
sdta2021-04-01 22:00
以下是引用ycvf在2021-4-1 21:57:55的发言:

能举例说明吗?

? GETWORDNUM("A,B,CW, D890,    123E", 5, ",")

[此贴子已经被作者于2021-4-1 22:04编辑过]

#31
吹水佬2021-04-01 22:10
题目增加时间也增加就正常,增加多少的原因就不好说。
#32
zhousr2021-04-01 22:16
快来围观,快来围观,神仙打架,神仙打架
#33
吹水佬2021-04-02 05:00
表结构还可以优化,JMSS1字段改为:
只有本站会员才能查看附件,请 登录

程序代码:
USE sp
n = GETWORDCOUNT(jmss1, ",")
COPY TO tmp.txt FIELDS jmss1 SDF
CREATE TABLE tt (f1 N(4,2))
FOR i=2 TO n
    ALTER TABLE tt ADD ("f"+TRANSFORM(i)) N(4,2)
ENDFOR
APPEND FROM tmp.txt TYPE DELIMITED WITH ","
BROWSE


这样速度应该会快点
程序代码:
t1 = SECONDS()
DIMENSION az[100]
USE tt
FOR i=1 TO FCOUNT()
    STORE 0 TO az
    COPY TO ARRAY arr FIELDS (FIELD(i))
    FOR j=1 TO ALEN(arr)
        k = (arr[j]+1)*10
        az[k] = az[k]+1
    ENDFOR
ENDFOR
? SECONDS() - t1
FOR i=1 TO 100
    IF az[i] > 0
        ? i/10-1, az[i]
    ENDIF
ENDFOR


[此贴子已经被作者于2021-4-2 06:15编辑过]

#34
schtg2021-04-02 05:24
@吹版,学习啦,谢谢!
#35
sdta2021-04-02 06:53
回复 33楼 吹水佬
吹版第一部分的代码,我也曾有过类似想法,后放弃,不过还是谢谢吹版。
#36
mywisdom882021-04-02 08:40
我运行吹版的,也就0.8秒
但楼主的,就要190多秒
#37
sdta2021-04-02 08:47
以下是引用mywisdom88在2021-4-2 08:40:25的发言:

我运行吹版的,也就0.8秒
但楼主的,就要190多秒

可能与工作区转换有关系,这种情况我也遇到过,具体原因说不清楚。
#38
sdta2021-04-04 09:31
谢谢大家的帮助、关注
如果有不同想法的朋友可以继续留言
#39
esailor2023-10-24 10:55
各位是VFP领域的高手,学习啦!
1