goto問題

迷霧綠洲發表於2016-08-04

這兩天幫著review一段程式碼,正好看到下面的一個函式,

UINT32 fh_i2c_read(UINT32 device, UINT32 raddr, UINT32 mode)
{
    UINT32 high_rdata, low_rdata;
    UINT8 high_raddr = (raddr>>8)&0xff;
    UINT8 low_raddr = raddr&0xff;
    UINT32 i2cDataCmd, i2cStatus;
  //    UINT32 valid;
    i2cDataCmd = 0x98600010;
    i2cStatus = 0x98600070;

    UINT32 error_cnt = 0;

    high_rdata = low_rdata = 0;

    REREAD:
    i2c_init(device);
    _ASM("flag 0"); // int disable
    if ( ((mode>>1) & 1) == 0 )
    {
        outw(i2cDataCmd,low_raddr);
    }
    else
    {
        outw(i2cDataCmd,high_raddr);
        outw(i2cDataCmd,low_raddr);
    }
    _ASM("flag 6");
    while( 4!=(inw(i2cStatus) & 0x5) );
    _ASM("flag 0"); // int disable
    if ( (mode & 1) == 0 )
    {
        outw(i2cDataCmd,0x100);     //issue read
    }
    else
    {
        outw(i2cDataCmd,0x100);
        outw(i2cDataCmd,0x100);
    }
    _ASM("flag 6");

    if ( (mode & 1) == 0 )
    {
        while( 0x8 !=(inw(i2cStatus) & 0x9) );  //recevie FIFO not empty
        low_rdata = inw(i2cDataCmd);
    }
    else
    {
        while( 0x8 !=(inw(i2cStatus) & 0x9) );
        high_rdata = inw(i2cDataCmd);
        while( 0x8 !=(inw(i2cStatus) & 0x9) );
        low_rdata = inw(i2cDataCmd);
    }
    wait(1000);
    if( 0x0 !=(inw(i2cStatus) & 0x9) )
    {
        error_cnt++;
        i2c_disable();
        if(error_cnt <= TRY_CNT)
        {
            goto REREAD;
        }
        else
        {
            timeout = i2c_read_timeout;
            return i2c_read_timeout;
        }
    }
    i2c_disable();

    return((high_rdata<<8)|low_rdata);
}

看過覺得這段程式碼中出現一個很熟悉的關鍵字 goto,學c語言都應該有被無數次的教導過不要用goto的經歷。到底為什麼不建議用goto語句呢,當時也沒有仔細研究,現在想來應該有以下幾個原因:

可讀性差

對於大量使用goto的程式碼,極端情況下的十幾行程式碼五六個goto跳轉的根本無法記住整個流程的順序結構。對於沒有IDE幫助,函式又非常長的情況,這種跳轉函式看起來需要不停的上下翻頁,除了作者接觸程式碼的都會瘋甚至發生流血事件。

可維護性差

上面已經說了不容易看懂,既然不容易看懂就更談不上改了。程式碼裡跳轉太多,在裡面新增任何邏輯都有可能導致程式會無法完成正常運轉。goto需要的跳轉標籤需要在行首,在不規範的程式碼很容易淹沒在正常邏輯中,容易在標籤的命名上產生重複。

流程可控性差

現在的軟體開發一般都不會是單兵作戰了,多人協作過程中忌諱使用複雜的邏輯流程,過程複雜很容易讓協作者不容易進入狀態。

效能損失

goto是由硬體彙編衍生過來的,對應彙編的jump或者long jump。但是在過去時間裡面cpu的主頻比較低,但是還是會有基本的流水線結構,一般執行當前指令時,會預取兩條指令儲存在暫存器中。但是goto回直接讓cpu進行跳轉,這樣的結果就是預取的指令用不上了,然後需要重新預取指令進行執行。早一些的cpu的跳轉能力還很差,多次的跳轉會讓cpu效能損失比較嚴重。

是不是goto就一無是處呢?

當然不是!對應linux kernel中程式碼看一下AT32AP700x的rtc程式碼


static int __init at32_rtc_probe(struct platform_device *pdev)
{
    struct resource *regs;
    struct rtc_at32ap700x *rtc;
    int irq;
    int ret;

    rtc = kzalloc(sizeof(struct rtc_at32ap700x), GFP_KERNEL);
    if (!rtc) {
        dev_dbg(&pdev->dev, "out of memory\n");
        return -ENOMEM;
    }

    regs = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    if (!regs) {
        dev_dbg(&pdev->dev, "no mmio resource defined\n");
        ret = -ENXIO;
        goto out;
    }

    irq = platform_get_irq(pdev, 0);
    if (irq <= 0) {
        dev_dbg(&pdev->dev, "could not get irq\n");
        ret = -ENXIO;
        goto out;
    }

    rtc->irq = irq;
    rtc->regs = ioremap(regs->start, regs->end - regs->start + 1);
    if (!rtc->regs) {
        ret = -ENOMEM;
        dev_dbg(&pdev->dev, "could not map I/O memory\n");
        goto out;
    }
    spin_lock_init(&rtc->lock);

    /*
     * Maybe init RTC: count from zero at 1 Hz, disable wrap irq.
     *
     * Do not reset VAL register, as it can hold an old time
     * from last JTAG reset.
     */
    if (!(rtc_readl(rtc, CTRL) & RTC_BIT(CTRL_EN))) {
        rtc_writel(rtc, CTRL, RTC_BIT(CTRL_PCLR));
        rtc_writel(rtc, IDR, RTC_BIT(IDR_TOPI));
        rtc_writel(rtc, CTRL, RTC_BF(CTRL_PSEL, 0xe)
                | RTC_BIT(CTRL_EN));
    }

    ret = request_irq(irq, at32_rtc_interrupt, IRQF_SHARED, "rtc", rtc);
    if (ret) {
        dev_dbg(&pdev->dev, "could not request irq %d\n", irq);
        goto out_iounmap;
    }

    platform_set_drvdata(pdev, rtc);

    rtc->rtc = rtc_device_register(pdev->name, &pdev->dev,
                &at32_rtc_ops, THIS_MODULE);
    if (IS_ERR(rtc->rtc)) {
        dev_dbg(&pdev->dev, "could not register rtc device\n");
        ret = PTR_ERR(rtc->rtc);
        goto out_free_irq;
    }

    device_init_wakeup(&pdev->dev, 1);

    dev_info(&pdev->dev, "Atmel RTC for AT32AP700x at %08lx irq %ld\n",
            (unsigned long)rtc->regs, rtc->irq);

    return 0;

out_free_irq:
    platform_set_drvdata(pdev, NULL);
    free_irq(irq, rtc);
out_iounmap:
    iounmap(rtc->regs);
out:
    kfree(rtc);
    return ret;
}

從中我們可以總結goto的幾個可以使用的情況:

一個函式中多次執行程式片段

可以看到上面的程式中對於初始化失敗後需要執行錯誤處理,有幾個地方出錯處理是一樣,但是又不想把出錯處理包裝成函式,把出錯處理程式在每個需要的地方複製貼上又太傻,goto這時候就很有價值。

可以當程式中註釋

void func() {
  int x;
  ......

  if (x)
    goto err;

err:
    ....

}

在註釋的收很容易遇到字符集的問題,不同的作業系統下甚至不同的文字編輯器都會可能看到一堆亂碼。這樣的goto的標籤也起到了註釋的作用。這些標籤說明一個函式內程式片段的作用,將函式進行更小的模組化。

多層迴圈的跳出

程式如果存在多層的迴圈,在迴圈中如果想要退出的話可能需要多個的break才能實現跳出,但是goto卻是一個很簡化的方法。

int i,j;
for( i;i<xxx;i++){
    for(j;j<xxx;j++){

    if(xx)
        goto out;
    }
}
out:

goto 需要注意什麼

寫程式跟社會上的其他事情都一樣,專家大神講的都被奉為聖旨,嚴格遵守少數人訂立的規則,卻每天都在見到亂拳打死老師傅的情景。所以也就隨著自己認為的原則就好了。對一般水平的大眾來說goto可以用但是最好注意以下幾條:

  • 不要大量使用

  • 不要向前跳

  • 注意堆疊

最後

goto使用的討論一直持續,各路大神小妖都有自己的一套理由和結論。聽說還有人專門寫了論文來研究,個人覺的還是使用多了根據自己的情況來決定是否使用和怎麼使用吧。

相關文章