Почему этот код ведет себя по-разному при включенной или выключенной оптимизации?

У меня есть простой тестовый запуск для ошибки, который находится в моем модуле OpenPGP https://github.com/singpolyma/OpenPGP-Haskell/blob/master/Data/OpenPGP.hs:

module Main where

import Data.OpenPGP
import Data.Binary (encode, decode)

packet = EmbeddedSignaturePacket (signaturePacket 2 168 ECDSA SHA256 [] [SignatureCreationTimePacket 1013401916,IssuerPacket "36FE856F4219F1C7"] 48065 [MPI 4,MPI 11,MPI 60,MPI 69,MPI 37,MPI 33,MPI 18,MPI 72,MPI 41,MPI 36,MPI 43,MPI 41,MPI 53,MPI 9,MPI 53,MPI 35,MPI 3,MPI 40,MPI 14,MPI 79,MPI 1,MPI 4,MPI 51,MPI 23,MPI 62,MPI 62,MPI 62,MPI 7,MPI 68,MPI 51,MPI 13,MPI 49,MPI 8,MPI 64,MPI 32,MPI 50,MPI 59,MPI 17,MPI 43,MPI 12,MPI 67,MPI 5,MPI 67,MPI 5,MPI 25,MPI 63,MPI 0,MPI 53,MPI 2,MPI 36,MPI 83,MPI 39,MPI 54,MPI 65,MPI 54,MPI 35,MPI 62,MPI 63,MPI 26,MPI 4,MPI 82,MPI 57,MPI 85,MPI 71,MPI 43,MPI 77])

main = print $ decode (encode packet) == packet

Если вы скомпилируете это (на ghc 7.4.1) с помощью:

ghc -O0 -fforce-recomp --make t.hs

Он работает как положено (то есть печатает True), но если скомпилировать так:

ghc -O1 -fforce-recomp --make t.hs

или это:

ghc -O2 -fforce-recomp --make t.hs

Он напечатает False.

Я не использую никаких расширений (кроме тривиального использования CPP) или низкоуровневых или небезопасных вызовов, и поведение должно быть из моей библиотеки, а не зависимость, поскольку здесь перекомпилируется только мой код.


person singpolyma    schedule 11.09.2012    source источник
comment
Я могу воспроизвести эту ошибку в GHC 7.4.2.   -  person Thomas M. DuBuisson    schedule 11.09.2012
comment
Вы используете двоичный файл или хлопья, когда наблюдаете эту ошибку?   -  person Ben Millwood    schedule 11.09.2012


Ответы (2)


Это ошибка в вашем коде. Учитывать

MPI 63,MPI 0,MPI 53
       ^^^^^

и

instance BINARY_CLASS MPI where
    put (MPI i) = do
        put (((fromIntegral . B.length $ bytes) - 1) * 8
                + floor (logBase (2::Double) $ fromIntegral (bytes `B.index` 0))
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                + 1 :: Word16)
    putSomeByteString bytes
    where
    bytes = if B.null bytes' then B.singleton 0 else bytes'
    bytes' = B.reverse $ B.unfoldr (\x ->
                    if x == 0 then Nothing else
                           Just (fromIntegral x, x `shiftR` 8)
             ) (assertProp (>=0) i)

Теперь, если мы закодируем MPI 0, bytes' будет пустым, поэтому bytes = B.singleton 0 и, следовательно, bytes `B.index` 0 равны 0.

Но logBase 2 0 — это -Infinity, а floor хорошо определено только для конечных значений (в пределах диапазона целевого типа).

При компиляции без оптимизации floor использует битовый шаблон через decodeFloat. Тогда floor (logBase 2 0) дает 0 для всех стандартных целочисленных типов фиксированной ширины.

С оптимизацией активно правило перезаписи, и floor использует primop double2Int#, который возвращает все, что делает аппаратное обеспечение, на x86 соответственно. x86-64, это minBound :: Int, насколько я знаю, независимо от битового шаблона. Соответствующий код

floorDoubleInt :: Double -> Int
floorDoubleInt (D# x) =
    case double2Int# x of
      n | x <## int2Double# n   -> I# (n -# 1#)
        | otherwise             -> I# n

и, конечно же, -Infinity < int2Double minBound, поэтому значение становится minBound - 1, что обычно равно maxBound.

Конечно, это приводит к неправильному результату, так как теперь "длина", которая равна put для MPI 0, становится 0, а 0 байт, помещенный после поля "длина", интерпретируется как часть "длины" следующего MPI.

person Daniel Fischer    schedule 11.09.2012
comment
Спасибо! Я бы не ожидал, что поведение floor изменится с -O, но вы правы, что в моих предположениях все равно была ошибка. - person singpolyma; 11.09.2012
comment
Есть несколько мест, где правила перезаписи меняют поведение. В основном, когда все равно нет правильного результата, например, при значениях вне диапазона для floor и др. Но иногда даже в местах со значимыми результатами, например. (realToFrac :: Float -> Double) (0/0) производит -5.104235503814077e38 без оптимизаций, NaN с оптимизациями. В языковом отчете указано realToFrac = fromRational . toRational, что означает первое. Поскольку Rational на самом деле не может обрабатывать NaN и бесконечности, нет хорошего способа обработать их в этом преобразовании, и они затираются. Примоп сохраняет их. - person Daniel Fischer; 11.09.2012
comment
Верно, @C.A.McCann, но в этом случае NaN не добавляют удовольствия, кроме того, что вы уже получаете с бесконечностями. - person Daniel Fischer; 11.09.2012

Проблема связана с вашим экземпляром BINARY_CLASS для MPI. Если я изменюсь

main = do
  print packet
  print (decode (encode packet) :: SignatureSubpacket)
  print $ decode (encode packet) == packet

Я вижу вывод (скомпилированный с -O2)

EmbeddedSignaturePacket (SignaturePacket {version = 2, signature_type = 168, key_algorithm = ECDSA, hash_algorithm = SHA256, hashed_subpackets = [], unhashed_subpackets = [SignatureCreationTimePacket 1013401916,IssuerPacket "36FE856F4219F1C7"], hash_head = 48065, signature = [MPI 4,MPI 11,MPI 60,MPI 69,MPI 37,MPI 33,MPI 18,MPI 72,MPI 41,MPI 36,MPI 43,MPI 41,MPI 53,MPI 9,MPI 53,MPI 35,MPI 3,MPI 40,MPI 14,MPI 79,MPI 1,MPI 4,MPI 51,MPI 23,MPI 62,MPI 62,MPI 62,MPI 7,MPI 68,MPI 51,MPI 13,MPI 49,MPI 8,MPI 64,MPI 32,MPI 50,MPI 59,MPI 17,MPI 43,MPI 12,MPI 67,MPI 5,MPI 67,MPI 5,MPI 25,MPI 63,MPI 0,MPI 53,MPI 2,MPI 36,MPI 83,MPI 39,MPI 54,MPI 65,MPI 54,MPI 35,MPI 62,MPI 63,MPI 26,MPI 4,MPI 82,MPI 57,MPI 85,MPI 71,MPI 43,MPI 77], trailer = Chunk "\168" (Chunk "<gI<" Empty)})
EmbeddedSignaturePacket (SignaturePacket {version = 2, signature_type = 168, key_algorithm = ECDSA, hash_algorithm = SHA256, hashed_subpackets = [], unhashed_subpackets = [SignatureCreationTimePacket 1013401916,IssuerPacket "36FE856F4219F1C7"], hash_head = 48065, signature = [MPI 4,MPI 11,MPI 60,MPI 69,MPI 37,MPI 33,MPI 18,MPI 72,MPI 41,MPI 36,MPI 43,MPI 41,MPI 53,MPI 9,MPI 53,MPI 35,MPI 3,MPI 40,MPI 14,MPI 79,MPI 1,MPI 4,MPI 51,MPI 23,MPI 62,MPI 62,MPI 62,MPI 7,MPI 68,MPI 51,MPI 13,MPI 49,MPI 8,MPI 64,MPI 32,MPI 50,MPI 59,MPI 17,MPI 43,MPI 12,MPI 67,MPI 5,MPI 67,MPI 5,MPI 25,MPI 63,MPI 0,MPI 0,MPI 339782829898145924110968965855122255180100961470274369007196973863828909184332476115285611703086303618816635857833592912611149], trailer = Chunk "\168" (Chunk "<gI<" Empty)})

Изменение вашего экземпляра MPI на эту более простую реализацию:

newtype MPI = MPI Integer deriving (Show, Read, Eq, Ord)
instance BINARY_CLASS MPI where
  put (MPI i) = do
    put (fromIntegral $ B.length bytes :: Word16)
    putSomeByteString bytes
    where
    bytes = if B.null bytes' then B.singleton 0 else bytes'
    bytes' = B.pack . map (read . (:[])) $ show i
  get = do
    length <- fmap fromIntegral (get :: Get Word16)
    bytes <- getSomeByteString length
    return (MPI $ read $ concatMap show $ B.unpack bytes)

устраняет проблему.

Есть несколько вещей, которые могут быть источником проблемы. Возможно, ваш код правильный (я не проверял это так или иначе), и в этом случае GHC выполняет какое-то недопустимое преобразование, ведущее где-то к переполнению/недополнению. Также возможно, что ваш код делает что-то неправильное, что проявляется только при определенных оптимизациях.

person John L    schedule 11.09.2012