我怎样才能加快这个非常慢的Shell脚本的数据制表与不确定性?

时间:2016-07-06 14:12:13

标签: bash performance shell statistics rounding

我正在使用通常具有平均值和不确定性的大型数据集。通常在发布时,仅显示单个数字的不确定性,并且相应的值四舍五入到该小数位。然后将不确定性包装在括号中并附加到缩短的平均字符串上。

例如:

  

平均值:101.0513213标准差:0.33129

......会给:

  

101.1(3)

在实践中,这听起来很简单,但实际上它有点复杂,因为你必须首先计算单个数字的标准偏差,然后用它来确定你将平均值四舍五入到的十进制数字。在舍入到10的情况下添加(即0.094轮次到0.09,但是0.095轮到0.1将数字更改为舍入到)以及事实你是四舍五入而不是截断,原则上实施起来有些繁琐。

我有一组BASH Script个功能,可以使用printfbcsedecho个调用来实现这一功能。它有效,但结果计算速度很慢。这是您可以自己尝试的样本。你应该能够看到它的速度有多慢:

#!/bin/bash

function CleanBC() {
    echo "${1/[eE][+][0]/*10^}" | bc -l | \
        sed -e 's#^\(-*\)\.#\10\.#g'
}

function Log10Float() {
    echo $( CleanBC "l($1)/l(10)" )
}

function TruncateDecimal() {
    echo $1 | sed -e 's#\.[0-9]*##g' 
}

function PowerOf10() {
    absPow=$( echo $1 | sed 's#-##g' )
    if [[ $( CleanBC "$1==0" ) -eq '1' ]]; then
        echo "1"
    elif [[ $( CleanBC "$1>0" ) -eq '1' ]]; then
        echo "1"$(printf '0%.0s' $( seq 1 $absPow ) )
    elif [[ $( CleanBC "$1==-1" ) -eq '1' ]]; then
        echo "0.1"
    elif [[ $( CleanBC "$1<-1" ) -eq '1' ]]; then
        echo "0."$(printf '0%.0s' $( seq 2 $absPow ) )"1"
    fi
}

function RoundArbitraryDigit() {
    pow=$( PowerOf10 $2)
    offset=$( CleanBC "if ($1>=0) {0.5} else {-0.5}" )
    absPow=$( echo $2 | sed -e 's#-##g' )
    invPow=$( PowerOf10 $( CleanBC "$2*-1" ) )
    shiftedVal=$( TruncateDecimal $( CleanBC "$invPow*$1+$offset" ) )
    val=$( CleanBC "scale=15;$shiftedVal*$pow" )
    echo $( printf "%.${absPow}f" $val )
}

function Flt2Int() {
    RoundArbitraryDigit $1 0 
}

function Round() {
    for v in ${@:3}; do
        div=$( CleanBC "$v / $2" )

        case $( echo $1 | tr '[:lower:]' '[:upper:]' ) in
            CLOSEST)
                val=$( TruncateDecimal $( Flt2Int $div ) );
                ;;
            UP)
                val=$( TruncateDecimal $div );
                ((val++))
                ;;
            DOWN)
                val=$( TruncateDecimal $div );
                ;;
        esac
        echo $( CleanBC "$val * $2" )
    done
}

function Tabulate() {
    roundTo=$( Log10Float $2 )
    roundTo=$( CleanBC "if ($roundTo < 0) {$roundTo -1} else {$roundTo}" )
    roundTo=$( TruncateDecimal $roundTo )
    roundedSD=$( RoundArbitraryDigit $2 $roundTo )
    invPow=$( PowerOf10 $( CleanBC "$roundTo*-1" ) )
    if [[ $( CleanBC "($invPow * $roundedSD) == 10" ) -eq '1' ]]; then
        ((roundTo++))
        roundedSD=$( RoundArbitraryDigit $roundedSD $roundTo )
        invPow=$( PowerOf10 $( CleanBC "$roundTo*-1" ) )
    fi
    intSD=$( CleanBC "($invPow * $roundedSD)" | sed -e 's#\.[0-9]*##g' )
    pow=$( PowerOf10 $roundTo )
    intSD=$( CleanBC "if ($pow > 10 ) {$pow*$intSD} else {$intSD}" )
    val="$( RoundArbitraryDigit $1 $roundTo )"
    if  [[ $( CleanBC "$roundTo > -1" ) -eq '1' ]]; then
        val=$( echo $val | sed -e 's#\.0*$##g' )
    fi
    echo "$val(${intSD})"
}

Tabulate '-.9782000' '0.0051335'
Tabulate '105.843516' '8.7571141'
Tabulate '0.2581699' '0.0020283'
Tabulate '3.4368211' '0.0739912'

我的第一个问题是特定功能或代码块是否比其他功能更慢地减慢整体计算。

其次,我想在第一个答案的基础上提出有关如何提高整体代码速度的建议。

第三,作为一个更普遍的问题,我想知道在这种情况下可以使用哪些工具来分析bash脚本并识别瓶颈。

(注意:使用CleanBC函数是因为有时用例中的其他相关函数会生成科学记数,即2.41321E+05等。因此,此函数是保持bc的必要条件。从失败 - 我的用例中的另一个要求。)

感谢@Gordon Davisson的建议我改进了我的剧本。为了更好地计时并检测另一个边缘情况,我将旧脚本的结束行修改为:

function test() {
    Tabulate '-.9782000' '0.0051335'
    Tabulate '105.843516' '8.7571141'
    Tabulate '1055.843516' '85.7571141'
    Tabulate '0.2581699' '0.0020283'
    Tabulate '3.4368211' '0.0739912'
}

time test

使用旧脚本我得到了:

real    0m12.627s
user    0m3.150s
sys     0m9.282s

新脚本是:

#!/bin/bash

function CleanBC() {
    val=$(bc -l <<< "${1/*[eE][+][0]/*10^}")
    if [[ $val == -* ]]; then
        echo "${val/#-./-0.}"
    else
        echo "${val/#./0.}"
    fi
}

function Log10Float() {
    CleanBC "l($1)/l(10)"
}

function TruncateDecimal() {
    echo ${1/.*/}
}

function PowerOf10() {
    case $1 in
        10) echo "10000000000" ;;
        9) echo "1000000000" ;;
        8) echo "100000000" ;;
        7) echo "10000000" ;;
        6) echo "1000000" ;;
        5) echo "100000" ;;
        4) echo "10000" ;;
        3) echo "1000" ;;
        2) echo "100" ;;
        1) echo "10" ;;
        0) echo "1" ;;
        -1) echo "0.1" ;;
        -2) echo "0.01" ;;
        -3) echo "0.001" ;;
        -4) echo "0.0001" ;;
        -5) echo "0.00001" ;;
        -6) echo "0.000001" ;;
        -7) echo "0.0000001" ;;
        -8) echo "0.00000001" ;;
        -9) echo "0.000000001" ;;
        -10) echo "0.0000000001" ;;
    esac
}

function RoundArbitraryDigit() {
    pow=$( PowerOf10 $2 )
    absPow=$2;
    absPow=${absPow/#-/}
    if [[ $1 == -* ]]; then
        offset=-0.5
    else
        offset=0.5
    fi
    if [[ $2 == -* ]]; then
        invPow=$( PowerOf10 $absPow )
    elif [[ $2 == 0 ]]; then
        invPow="1"
    else
        invPow=$( PowerOf10 "-${2}" )
    fi
    shiftedVal=$( CleanBC "$invPow*$1+$offset" )
    shiftedVal=${shiftedVal/.*/}
    val=$( CleanBC "scale=15;$shiftedVal*$pow" )
    #printf "%.${absPow}f" $val
    echo $val
}

function Flt2Int() {
    RoundArbitraryDigit $1 0 
}

function Round() {
    for v in ${@:3}; do
        div=$( CleanBC "$v / $2" )

        case "${1^^}" in
            CLOSEST)
                val=$( Flt2Int $div );
                ;;
            UP)
                #truncate the decimal
                val=${div/.*/}
                ((val++))
                ;;
            DOWN)
                #truncate the decimal
                val=${div/.*/}
                ;;
        esac
        CleanBC "$val * $2"
    done
}

function Tabulate() {
    roundTo=$( Log10Float $2 )
    if [[ $roundTo == -* ]]; then
        roundTo=$( CleanBC "$roundTo -1" )
    fi
    roundTo=${roundTo/.*/}
    roundedSD=$( RoundArbitraryDigit $2 $roundTo )
    if [[ $roundTo == -* ]]; then
        invPow=$( PowerOf10 ${roundTo/#-/} )
    elif [[ $roundTo == 0 ]]; then
        invPow="1"
    else
        invPow=$( PowerOf10 "-${roundTo}" )
    fi
    if [[ $( CleanBC "($invPow * $roundedSD)" ) == "10" ]]; then
        ((roundTo++))
        roundedSD=$( RoundArbitraryDigit $roundedSD $roundTo )
        if [[ $roundTo == -* ]]; then
            invPow=$( PowerOf10 ${roundTo/#-/} )
        elif [[ $roundTo == 0 ]]; then
            invPow="1"
        else
            invPow=$( PowerOf10 "-${roundTo}" )
        fi
    fi
    intSD=$( CleanBC "($invPow * $roundedSD)" | sed -e 's#\.[0-9]*##g' )
    pow=$( PowerOf10 $roundTo )
    if [[ $pow != 0.* ]] && [[ $pow != "1" ]]; then
        intSD=$( CleanBC "$pow*$intSD" )
    fi
    val="$( RoundArbitraryDigit $1 $roundTo )"
    if [[ $roundTo != -* ]]; then
        echo "${val/.*/}(${intSD})"
    else
        echo "${val}(${intSD})"
    fi
}

function test() {
    Tabulate '-.9782000' '0.0051335'
    Tabulate '105.843516' '8.7571141'
    Tabulate '1055.843516' '85.7571141'
    Tabulate '0.2581699' '0.0020283'
    Tabulate '3.4368211' '0.0739912'
}

time test

这里的关键区别在于我删除了一些多余的shell调用,用字符串操作替换了其他调用(包括消除基于bc的条件逻辑)。新时间是:

real    0m2.566s
user    0m0.605s
sys     0m1.619s

这大约是加速的五倍!

虽然我仍在考虑移植到Python脚本(正如他所建议的那样),但现在我对我的结果非常满意,这将我的制表脚本运行时间从大约5小时缩短到大约一小时。

1 个答案:

答案 0 :(得分:1)

最好的解决方案是用实际支持本机浮点数学的语言编写脚本,即除了bash之外的任何东西。我将推荐第二个cdarke推荐的Python,但实际上几乎所有东西都比shell脚本更好。

最大的原因是shell实际上并没有太多的功能;它真正擅长的是启动其他程序(bcsed等)来完成实际工作。但启动程序的计算成本非常高。在shell中,任何涉及外部命令,管道或$( ... )(或其反引号等价物)的东西都需要创建一个新进程,这应该是一些简单的计算中间的大量开销。比较这两个片段:

for ((num=1; num<10000; num++)); do
    result="$(echo "$i" | sed 's#0##g')"
done

for ((num=1; num<10000; num++)); do
    result="${num//0/}"
done

他们都做同样的事情(遍历所有数字从1到10,000,然后将result设置为删除所有“0”的数字)。在我的电脑上,第一个耗时36秒,而第二个耗时0.2秒。原因很简单:在第二种情况下,所有操作都直接在bash中完成,无需创建其他进程。另一方面,第一个必须创建一个子shell(即运行bash的另一个进程)来运行$( ... )的内容,然后另一个子shell来执行echo,然后处理运行sed进行替换。这是计算机每次通过循环执行的三个流程创建(以及退出/清理)。这就是为什么第一个比第二个慢100多倍。

考虑第三个片段:

TrimZeroes() {
    echo "${1//0/}"
}

for ((num=1; num<10000; num++)); do
    result="$(TrimZeroes "$num")"
done

看起来像第二个更干净(更好抽象)的版本,对吗?花了8秒钟,因为$( ... )需要创建一个子shell来运行TrimZeroes

现在,查看脚本中的一行:

roundTo=$( Log10Float $2 )

这会创建一个子shell来运行Log10Float。它由单行

组成
echo $( CleanBC "l($1)/l(10)" )

...创建另一个子shell以运行CleanBC,其中包含:

echo "${1/[eE][+][0]/*10^}" | bc -l | sed -e 's#^\(-*\)\.#\10\.#g'

...它创建了三个进程,一个用于管道的每个部分。这是一个采用一个对数的总共五个过程!

因此,您可以采取一些措施来加速脚本:主要是切换到使用bash的内置字符串操作功能,并尽可能多地内联函数调用。但是这会使脚本比现在更加混乱,并且它仍然比用更合适的语言写得慢得多。

Python很好。 Ruby非常棒。即使是perl也会比shell更好。