Commit 998803dc authored by Andrzej Rybczak's avatar Andrzej Rybczak

Add flags for annotating Generic{,1} methods INLINE[1] (#11068)

Makes it possible for GHC to optimize away intermediate Generic representation
for more types.

Metric Increase:
    T12227
parent 3d7db148
Pipeline #26385 passed with stages
in 401 minutes and 59 seconds
......@@ -158,6 +158,8 @@ data GeneralFlag
| Opt_Specialise
| Opt_SpecialiseAggressively
| Opt_CrossModuleSpecialise
| Opt_InlineGenerics
| Opt_InlineGenericsAggressively
| Opt_StaticArgumentTransformation
| Opt_CSE
| Opt_StgCSE
......
......@@ -3468,6 +3468,8 @@ fFlagsDeps = [
flagSpec "specialize-aggressively" Opt_SpecialiseAggressively,
flagSpec "cross-module-specialise" Opt_CrossModuleSpecialise,
flagSpec "cross-module-specialize" Opt_CrossModuleSpecialise,
flagSpec "inline-generics" Opt_InlineGenerics,
flagSpec "inline-generics-aggressively" Opt_InlineGenericsAggressively,
flagSpec "static-argument-transformation" Opt_StaticArgumentTransformation,
flagSpec "strictness" Opt_Strictness,
flagSpec "use-rpaths" Opt_RPath,
......@@ -3981,6 +3983,7 @@ optLevelFlags -- see Note [Documenting optimisation flags]
, ([1,2], Opt_Specialise)
, ([1,2], Opt_CrossModuleSpecialise)
, ([1,2], Opt_InlineGenerics)
, ([1,2], Opt_Strictness)
, ([1,2], Opt_UnboxSmallStrictFields)
, ([1,2], Opt_CprAnal)
......
......@@ -2049,8 +2049,7 @@ genDerivStuff mechanism loc clas inst_tys tyvars
, dit_rep_tc_args = rep_tc_args
}
, dsm_stock_gen_fn = gen_fn }
-> do (binds, faminsts, field_names) <- gen_fn loc rep_tc rep_tc_args inst_tys
pure (binds, [], faminsts, field_names)
-> gen_fn loc rep_tc rep_tc_args inst_tys
-- Try DeriveAnyClass
DerivSpecAnyClass -> do
......
......@@ -43,6 +43,7 @@ import GHC.Builtin.Types
import GHC.Builtin.Names
import GHC.Tc.Utils.Env
import GHC.Tc.Utils.Monad
import GHC.Driver.Session
import GHC.Driver.Types
import GHC.Utils.Error( Validity(..), andValid )
import GHC.Types.SrcLoc
......@@ -76,10 +77,12 @@ For the generic representation we need to generate:
-}
gen_Generic_binds :: GenericKind -> TyCon -> [Type]
-> TcM (LHsBinds GhcPs, FamInst)
-> TcM (LHsBinds GhcPs, [LSig GhcPs], FamInst)
gen_Generic_binds gk tc inst_tys = do
dflags <- getDynFlags
repTyInsts <- tc_mkRepFamInsts gk tc inst_tys
return (mkBindsRep gk tc, repTyInsts)
let (binds, sigs) = mkBindsRep dflags gk tc
return (binds, sigs, repTyInsts)
{-
************************************************************************
......@@ -332,12 +335,33 @@ gk2gkDC Gen1_{} d = Gen1_DC $ last $ dataConUnivTyVars d
-- Bindings for the Generic instance
mkBindsRep :: GenericKind -> TyCon -> LHsBinds GhcPs
mkBindsRep gk tycon =
unitBag (mkRdrFunBind (L loc from01_RDR) [from_eqn])
`unionBags`
unitBag (mkRdrFunBind (L loc to01_RDR) [to_eqn])
mkBindsRep :: DynFlags -> GenericKind -> TyCon -> (LHsBinds GhcPs, [LSig GhcPs])
mkBindsRep dflags gk tycon = (binds, sigs)
where
binds = unitBag (mkRdrFunBind (L loc from01_RDR) [from_eqn])
`unionBags`
unitBag (mkRdrFunBind (L loc to01_RDR) [to_eqn])
-- See Note [Generics performance tricks]
sigs = if gopt Opt_InlineGenericsAggressively dflags
|| (gopt Opt_InlineGenerics dflags && inlining_useful)
then [inline1 from01_RDR, inline1 to01_RDR]
else []
where
inlining_useful
| cons <= 1 = True
| cons <= 4 = max_fields <= 5
| cons <= 8 = max_fields <= 2
| cons <= 16 = max_fields <= 1
| cons <= 24 = max_fields == 0
| otherwise = False
where
cons = length datacons
max_fields = maximum $ map dataConSourceArity datacons
inline1 f = L loc . InlineSig noExtField (L loc f)
$ alwaysInlinePragma { inl_act = ActiveAfter NoSourceText 1 }
-- The topmost M1 (the datatype metadata) has the exact same type
-- across all cases of a from/to definition, and can be factored out
-- to save some allocations during typechecking.
......@@ -1039,4 +1063,48 @@ factor it out reduce the typechecker's burden:
A simple change, but one that pays off, since it goes turns an O(n) amount of
coercions to an O(1) amount.
Note [Generics performance tricks]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Generics-based algorithms tend to rely on GHC optimizing away the intermediate
representation for optimal performance. However, the default unfolding threshold
is usually too small for GHC to do that.
The recommended approach thus far was to increase unfolding threshold, but this
makes GHC inline more aggressively in general, whereas it should only be more
aggresive with generics-based code.
The solution is to use a heuristic that'll annotate Generic class methods with
INLINE[1] pragmas (the explicit phase is used to give users phase control as
they can annotate their functions with INLINE[2] or INLINE[0] if appropriate).
The current heuristic was chosen by looking at how annotating Generic methods
INLINE[1] helps with optimal code generation for several types of generic
algorithms:
* Round trip through the generic representation.
* Generation of NFData instances.
* Generation of field lenses.
The experimentation was done by picking data types having N constructors with M
fields each and using their derived Generic instances to generate code with the
above algorithms.
The results are threshold values for N and M (contained in
`mkBindsRep.inlining_useful`) for which inlining is beneficial, i.e. it usually
leads to performance improvements at both compile time (the simplifier has to do
more work, but then there's much less code left for subsequent phases to work
with) and run time (the generic representation of a data type is optimized
away).
The T11068 test case, which includes the algorithms mentioned above, tests that
the generic representations of several data types optimize away using the
threshold values in `mkBindsRep.inlining_useful`.
If one uses threshold values higher what is found in
`mkBindsRep.inlining_useful`, then annotating Generic class methods with INLINE
pragmas tends to be at best useless and at worst lead to code size blowup
without runtime performance improvements.
-}
......@@ -222,19 +222,22 @@ data DerivSpecMechanism
SrcSpan -> TyCon -- dit_rep_tc
-> [Type] -- dit_rep_tc_args
-> [Type] -- inst_tys
-> TcM (LHsBinds GhcPs, BagDerivStuff, [Name])
-- ^ This function returns three things:
-> TcM (LHsBinds GhcPs, [LSig GhcPs], BagDerivStuff, [Name])
-- ^ This function returns four things:
--
-- 1. @LHsBinds GhcPs@: The derived instance's function bindings
-- (e.g., @compare (T x) (T y) = compare x y@)
--
-- 2. @BagDerivStuff@: Auxiliary bindings needed to support the derived
-- 2. @[LSig GhcPs]@: A list of instance specific signatures/pragmas.
-- Most likely INLINE pragmas for class methods.
--
-- 3. @BagDerivStuff@: Auxiliary bindings needed to support the derived
-- instance. As examples, derived 'Generic' instances require
-- associated type family instances, and derived 'Eq' and 'Ord'
-- instances require top-level @con2tag@ functions.
-- See @Note [Auxiliary binders]@ in "GHC.Tc.Deriv.Generate".
--
-- 3. @[Name]@: A list of Names for which @-Wunused-binds@ should be
-- 4. @[Name]@: A list of Names for which @-Wunused-binds@ should be
-- suppressed. This is used to suppress unused warnings for record
-- selectors when deriving 'Read', 'Show', or 'Generic'.
-- See @Note [Deriving and unused record selectors]@.
......@@ -427,7 +430,7 @@ instance Outputable DerivContext where
data OriginativeDerivStatus
= CanDeriveStock -- Stock class, can derive
(SrcSpan -> TyCon -> [Type] -> [Type]
-> TcM (LHsBinds GhcPs, BagDerivStuff, [Name]))
-> TcM (LHsBinds GhcPs, [LSig GhcPs], BagDerivStuff, [Name]))
| StockClassError SDoc -- Stock class, but can't do it
| CanDeriveAnyClass -- See Note [Deriving any class]
| NonDerivableClass SDoc -- Cannot derive with either stock or anyclass
......@@ -566,7 +569,7 @@ hasStockDeriving
-> TyCon
-> [Type]
-> [Type]
-> TcM (LHsBinds GhcPs, BagDerivStuff, [Name]))
-> TcM (LHsBinds GhcPs, [LSig GhcPs], BagDerivStuff, [Name]))
hasStockDeriving clas
= assocMaybe gen_list (getUnique clas)
where
......@@ -575,7 +578,7 @@ hasStockDeriving clas
-> TyCon
-> [Type]
-> [Type]
-> TcM (LHsBinds GhcPs, BagDerivStuff, [Name]))]
-> TcM (LHsBinds GhcPs, [LSig GhcPs], BagDerivStuff, [Name]))]
gen_list = [ (eqClassKey, simpleM gen_Eq_binds)
, (ordClassKey, simpleM gen_Ord_binds)
, (enumClassKey, simpleM gen_Enum_binds)
......@@ -593,7 +596,7 @@ hasStockDeriving clas
simple gen_fn loc tc tc_args _
= let (binds, deriv_stuff) = gen_fn loc tc tc_args
in return (binds, deriv_stuff, [])
in return (binds, [], deriv_stuff, [])
-- Like `simple`, but monadic. The only monadic thing that these functions
-- do is allocate new Uniques, which are used for generating the names of
......@@ -601,18 +604,18 @@ hasStockDeriving clas
-- See Note [Auxiliary binders] in GHC.Tc.Deriv.Generate.
simpleM gen_fn loc tc tc_args _
= do { (binds, deriv_stuff) <- gen_fn loc tc tc_args
; return (binds, deriv_stuff, []) }
; return (binds, [], deriv_stuff, []) }
read_or_show gen_fn loc tc tc_args _
= do { fix_env <- getDataConFixityFun tc
; let (binds, deriv_stuff) = gen_fn fix_env loc tc tc_args
field_names = all_field_names tc
; return (binds, deriv_stuff, field_names) }
; return (binds, [], deriv_stuff, field_names) }
generic gen_fn _ tc _ inst_tys
= do { (binds, faminst) <- gen_fn tc inst_tys
= do { (binds, sigs, faminst) <- gen_fn tc inst_tys
; let field_names = all_field_names tc
; return (binds, unitBag (DerivFamInst faminst), field_names) }
; return (binds, sigs, unitBag (DerivFamInst faminst), field_names) }
-- See Note [Deriving and unused record selectors]
all_field_names = map flSelector . concatMap dataConFieldLabels
......
......@@ -28,6 +28,12 @@ Compiler
since the argument was already forced in the first equation. For more
details see :ghc-flag:`-Wredundant-bang-patterns`.
- New ``-finline-generics`` and ``-finline-generics-aggressively`` flags for
improving performance of generics-based algorithms.
For more details see :ghc-flag:`-finline-generics` and
:ghc-flag:`-finline-generics-aggressively`.
- Type checker plugins which work with the natural numbers now
should use ``naturalTy`` kind instead of ``typeNatKind``, which has been removed.
......
......@@ -979,6 +979,50 @@ by saying ``-fno-wombat``.
which returns a constrained type. For example, a type class where one
of the methods implements a traversal.
.. ghc-flag:: -finline-generics
:shortdesc: Annotate methods of derived Generic and Generic1 instances with
INLINE[1] pragmas based on heuristics. Implied by :ghc-flag:`-O`.
:type: dynamic
:reverse: -fno-inline-generics
:category:
:default: on
:since: 9.2.1
.. index::
single: inlining, controlling
single: unfolding, controlling
Annotate methods of derived Generic and Generic1 instances with INLINE[1]
pragmas based on heuristics dependent on the size of the data type in
question. Improves performance of generics-based algorithms as GHC is able
to optimize away intermediate representation more often.
.. ghc-flag:: -finline-generics-aggressively
:shortdesc: Annotate methods of all derived Generic and Generic1 instances
with INLINE[1] pragmas.
:type: dynamic
:reverse: -fno-inline-generics-aggressively
:category:
:default: off
:since: 9.2.1
.. index::
single: inlining, controlling
single: unfolding, controlling
Annotate methods of all derived Generic and Generic1 instances with
INLINE[1] pragmas.
This flag should only be used in modules deriving Generic instances that
weren't considered appropriate for INLINE[1] annotations by heuristics of
:ghc-flag:`-finline-generics`, yet you know that doing so would be
beneficial.
When enabled globally it will most likely lead to worse compile times and
code size blowup without runtime performance gains.
.. ghc-flag:: -fsolve-constant-dicts
:shortdesc: When solving constraints, try to eagerly solve
super classes using available dictionaries.
......
{-# LANGUAGE DeriveGeneric #-}
{-# OPTIONS_GHC -finline-generics-aggressively #-}
module T11068_aggressive where
import GHC.Generics
-- For 2 data constructors -finline-generics annotates class methods of the
-- derived Generic instance as INLINE[1] only if each has at most 5 fields.
data X
= X1 Int Int Int Int Int Int Int Int Int Int
| X2 Int Int Int Int Int Int Int Int Int Int
deriving Generic
......@@ -65,6 +65,7 @@ test('T7947', [], multimod_compile, ['T7947', '-v0'])
test('T10561', normal, compile, [''])
test('T10487', [], multimod_compile, ['T10487', '-v0'])
test('T10524', normal, compile, [''])
test('T11068_aggressive', [normalise_errmsg_fun(just_the_deriving)], compile, ['-ddump-deriv -dsuppress-uniques'])
test('T11148', normal, makefile_test, [])
test('T9968', normal, compile, [''])
test('T9968a', normal, compile, [''])
......
......@@ -7,3 +7,8 @@ T4007:
$(RM) -f T4007.hi T4007.o
'$(TEST_HC)' $(TEST_HC_OPTS) -c -O -ddump-rule-firings T4007.hs
T11068:
$(RM) -f T11068a.hi T11068a.o T11068b.hi T11068b.o T11068.hi T11068.o
'$(TEST_HC)' $(TEST_HC_OPTS) -c -O T11068a.hs
'$(TEST_HC)' $(TEST_HC_OPTS) -c -O T11068b.hs
-'$(TEST_HC)' $(TEST_HC_OPTS) -c -O T11068.hs -ddump-simpl | grep 'Generic'
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeApplications #-}
module T11068 where
import Control.DeepSeq
import GHC.Generics
import T11068a
import T11068b
-- X1
instance NFData X1
x1_id :: X1 -> X1
x1_id = to . from
x1_lens :: Lens' X1 Integer
x1_lens = gfield @"x1_f1"
-- X1'
instance NFData X1'
x1'_id :: X1' -> X1'
x1'_id = to . from
x1'_lens :: Lens' X1' Integer
x1'_lens = gfield @"x1'_f1"
-- X4
instance NFData X4
x4_id :: X4 -> X4
x4_id = to . from
x4_lens :: Lens' X4 Integer
x4_lens = gfield @"x4_f1"
-- X4'
instance NFData X4'
x4'_id :: X4' -> X4'
x4'_id = to . from
x4'_lens :: Lens' X4' Integer
x4'_lens = gfield @"x4'_f1"
-- X8
instance NFData X8
x8_id :: X8 -> X8
x8_id = to . from
x8_lens :: Lens' X8 Integer
x8_lens = gfield @"x8_f1"
-- X8'
instance NFData X8'
x8'_id :: X8' -> X8'
x8'_id = to . from
x8'_lens :: Lens' X8' Integer
x8'_lens = gfield @"x8'_f1"
-- X12'
instance NFData X12'
-- id for data types with strict fields fully optimizes up to 12x1
x12'_id :: X12' -> X12'
x12'_id = to . from
x12'_lens :: Lens' X12' Integer
x12'_lens = gfield @"x12'_f1"
-- X16
instance NFData X16
x16_id :: X16 -> X16
x16_id = to . from
x16_lens :: Lens' X16 Integer
x16_lens = gfield @"x16_f1"
-- X16'
instance NFData X16'
x16'_lens :: Lens' X16' Integer
x16'_lens = gfield @"x16'_f1"
-- X24
instance NFData X24
x24_id :: X24 -> X24
x24_id = to . from
This diff is collapsed.
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
module T11068b (Lens', GField(..)) where
import Data.Kind
import Data.Type.Bool
import Data.Type.Equality
import GHC.Generics
import GHC.TypeLits
-- Code taken from the optics / generic-lens-lite library.
----------------------------------------
-- Profunctors
data Context a b t = Context (b -> t) a
deriving Functor
class Profunctor p where
dimap :: (a -> b) -> (c -> d) -> p b c -> p a d
lmap :: (a -> b) -> p b c -> p a c
rmap :: (c -> d) -> p b c -> p b d
class Profunctor p => Strong p where
first' :: p a b -> p (a, c) (b, c)
second' :: p a b -> p (c, a) (c, b)
linear :: LensVL s t a b -> p a b -> p s t
linear f = dimap
((\(Context bt a) -> (a, bt)) . f (Context id))
(\(b, bt) -> bt b)
. first'
{-# INLINE linear #-}
data Store a b s t = Store (s -> a) (s -> b -> t)
instance Profunctor (Store a b) where
dimap f g (Store get set) = Store (get . f) (\s -> g . set (f s))
lmap f (Store get set) = Store (get . f) (\s -> set (f s))
rmap g (Store get set) = Store get (\s -> g . set s)
instance Strong (Store a b) where
first' (Store get set) = Store (get . fst) (\(s, c) b -> (set s b, c))
second' (Store get set) = Store (get . snd) (\(c, s) b -> (c, set s b))
----------------------------------------
-- Lens
type LensVL s t a b = forall f. Functor f => (a -> f b) -> s -> f t
type LensVL' s a = LensVL s s a a
newtype Lens s t a b = Lens (forall p. Strong p => p a b -> p s t)
type Lens' s a = Lens s s a a
lens :: (s -> a) -> (s -> b -> t) -> Lens s t a b
lens get set = Lens $ dimap (\s -> (get s, s))
(\(b, s) -> set s b)
. first'
lensVL :: LensVL s t a b -> Lens s t a b
lensVL l = Lens (linear l)
withLens :: Lens s t a b -> ((s -> a) -> (s -> b -> t) -> r) -> r
withLens (Lens l) k = case l $ Store id (\_ -> id) of
Store get set -> k get set
----------------------------------------
-- Field
class GField (name :: Symbol) s a | name s -> a where
gfield :: Lens' s a
instance
( Generic s
, path ~ GetPathTree name (Rep s)
, GFieldSum name s path (Rep s) a
) => GField name s a where
gfield = withLens
(lensVL (\f s -> to <$> gfieldSum @name @s @path f (from s)))
(\get set -> lensVL $ \f s -> set s <$> f (get s))
{-# INLINE gfield #-}
data Void0
-- | Hidden instance.
instance a ~ Void0 => GField name Void0 a where
gfield = lensVL id
class GFieldSum (name :: Symbol) s (path :: PathTree) (g :: Type -> Type) a
| name g -> a where
gfieldSum :: LensVL' (g x) a
instance
( GFieldSum name s path V1 a
, TypeError ('Text "Type " ':<>: Quoted ('ShowType s) ':<>:
'Text " has no data constructors")
) => GFieldSum name s path V1 a where
gfieldSum = error "unreachable"
instance
( GFieldSum name s path g a
) => GFieldSum name s path (M1 D m g) a where
gfieldSum f (M1 x) = M1 <$> gfieldSum @name @s @path f x
instance
( GFieldSum name s path1 g1 a
, GFieldSum name s path2 g2 a
) => GFieldSum name s ('PathTree path1 path2) (g1 :+: g2) a where
gfieldSum f (L1 x) = L1 <$> gfieldSum @name @s @path1 f x
gfieldSum f (R1 y) = R1 <$> gfieldSum @name @s @path2 f y
{-# INLINE gfieldSum #-}
instance
( path ~ FromMaybe
(TypeError
('Text "Type " ':<>: Quoted ('ShowType s) ':<>:
'Text " doesn't have a field named " ':<>: Quoted ('Text name)))
mpath
, GFieldProd name s path g a
) => GFieldSum name s ('PathLeaf mpath) (M1 C m g) a where
gfieldSum f (M1 x) = M1 <$> gfieldProd @name @s @path f x
class GFieldProd (name :: Symbol) s (path :: [Path]) g a | name g -> a where
gfieldProd :: LensVL' (g x) a
instance
( GFieldProd name s path g1 a
) => GFieldProd name s ('PathLeft : path) (g1 :*: g2) a where
gfieldProd f (x :*: y) = (:*: y) <$> gfieldProd @name @s @path f x
instance
( GFieldProd name s path g2 a
) => GFieldProd name s ('PathRight : path) (g1 :*: g2) a where
gfieldProd f (x :*: y) = (x :*:) <$> gfieldProd @name @s @path f y
instance
( a ~ b -- for better error message if types don't match
) => GFieldProd name s '[] (M1 S ('MetaSel ('Just name) su ss ds) (Rec0 b)) a where
gfieldProd f (M1 (K1 x)) = M1 . K1 <$> f x
----------------------------------------
-- Helpers
type family Quoted (s :: ErrorMessage) :: ErrorMessage where
Quoted s = 'Text "‘" ':<>: s ':<>: 'Text "’"
data PathTree
= PathTree PathTree PathTree
| PathLeaf (Maybe [Path])
| NoPath
data Path = PathLeft | PathRight
-- | Compute paths to a field for a generic representation of a data type.
type family GetPathTree (name :: Symbol) g :: PathTree where
GetPathTree name (M1 D _ g) = GetPathTree name g
GetPathTree name V1 = 'NoPath
GetPathTree name (g1 :+: g2) = 'PathTree (GetPathTree name g1)
(GetPathTree name g2)
GetPathTree name (M1 C _ g) = 'PathLeaf (GetPath name g '[])
-- | Compute path to a constructor in a sum or a field in a product.
type family GetPath (name :: Symbol) g (acc :: [Path]) :: Maybe [Path] where
GetPath name (M1 D _ g) acc = GetPath name g acc
-- Find path to a constructor in a sum type
GetPath name (M1 C ('MetaCons name _ _) _) acc = 'Just (Reverse acc '[])
GetPath name (g1 :+: g2) acc = Alt (GetPath name g1 ('PathLeft : acc))
(GetPath name g2 ('PathRight : acc))
-- Find path to a field in a product type
GetPath name (M1 S ('MetaSel ('Just name) _ _ _) _) acc = 'Just (Reverse acc '[])
GetPath name (g1 :*: g2) acc = Alt (GetPath name g1 ('PathLeft : acc))
(GetPath name g2 ('PathRight : acc))
GetPath _ _ _ = 'Nothing
-- | Reverse a type-level list.
type family Reverse (xs :: [k]) (acc :: [k]) :: [k] where
Reverse '[] acc = acc
Reverse (x : xs) acc = Reverse xs (x : acc)
type family FromMaybe (def :: a) (m :: Maybe a) :: a where
FromMaybe _ ('Just a) = a
FromMaybe def 'Nothing = def
-- | Type-level mplus for 'Maybe'.
type family Alt (m1 :: Maybe a) (m2 :: Maybe a) :: Maybe a where
Alt ('Just a) _ = 'Just a
Alt _ b = b
......@@ -178,6 +178,8 @@ test('T10370',
compile,
[''])
test('T11068', normal, makefile_test, ['T11068'])
test('T10547',
[ collect_compiler_stats('bytes allocated', 4),
],
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment